# Easel source analysis: do not write the main loop—weld OpenClaw into a social-media content workbench

> **Subject**: Easel ([`ZJU-REAL/Easel`](https://github.com/ZJU-REAL/Easel)), based on **commit `765f5a6`** (2026-09-10 10:52:51 +0800, `main`). License **Apache-2.0**. Version number in `pyproject.toml` is **0.1.0**; `easel/__init__.py` is still `0.0.1`—the two have not yet aligned.
> **Code size** (sparse checkout, skipping `assets/` promo videos): **705 files**. The code that actually works is **`easel/` 9 Python files / 768 lines** + **`web/app.py` 2,509 lines** + **frontend `web/frontend/src` about 5,238 lines of TS/TSX** + **112 OpenClaw SKILLs** + **44 `skills/shared/scripts/*.py`**. Tests **650 lines**. The repo is large (about 300MB+ on GitHub); the vast majority is display material.
> **Community size**: **733 star / 90 fork / 2 open issue** (2026-09-10 GitHub API). Repo created **2026-08-28**—about two weeks on the analysis day. Zhejiang University REAL Lab + Peking University OpenDCAI. Homepage [zju-real.github.io/Easel](https://zju-real.github.io/Easel/).
> **One-line position**: **an open-source content workbench for domestic social-media creators**. It itself **does not write an Agent main loop**. It treats [OpenClaw](https://github.com/) as the execution engine, and with account personas, 112 executable SKILLs, and real-browser publishing, welds “discover → plan → produce → publish → attribute” into a pipeline that can land on disk and actually post.
> **Audience**: readers who have read any analysis in this series. **Please change axis first**: most earlier projects answer “how to make AI write code / do work”; Open Design answers “how not to write the loop, and treat others’ CLIs as the engine for design”; MiroFish answers “how to let a crowd of AIs rehearse the future.” Easel sits on the same side as Open Design—the **host**—but the object is not a design file. It is **Xiaohongshu cards, short videos, Zhihu answers, and the data after they are posted to a real platform**.

---

**Contents**

**Part I · What it is**
1. [Project overview: 700 stars in two weeks—what is it actually selling](#ch1)
2. [Full stack: a thin integration layer + a borrowed engine + a skill library](#ch2)
3. [Five-layer workflow: product narrative, not a runtime scheduler](#ch3)

**Part II · How the host catches the engine**
4. [Three entries, one source of truth: chat / skill / web](#ch4)
5. [Prompt four-layer stack: SOUL → AGENTS → CONTEXT → SKILL](#ch5)
6. [sync.sh: the workspace is the script; the project root is the worksite](#ch6)
7. [Timeouts, proxy, doctor: make the environment a checkable product](#ch7)

**Part III · Skills are the product**
8. [SKILL-SPEC: three-layer load and a 200-line cap](#ch8)
9. [112 Skills: read the library by layer](#ch9)
10. [outputs contract: content is a project, not a chat log](#ch10)
11. [Showcase: how xhs-note-creator makes a note](#ch11)

**Part IV · Real publish and the workbench**
12. [Two gates: content_guard hard-blocks; persona_gate only reminds](#ch12)
13. [Login and one-click publish: Playwright + SMS wall](#ch13)
14. [SSE chat: disconnect does not kill, dual lock, uuid5, thinking self-heal](#ch14)
15. [Frontend workbench: 11-page SPA and cross-tab sessions](#ch15)

**Part V · Evaluation**
16. [What is actually new](#ch16)
17. [Engineering quality: highlights and hard wounds](#ch17)
18. [Twelve lessons for building a new product](#ch18)
19. [Its place in this series](#ch19)
20. [🔍 Source pointer table](#ch20)

---

<h2 id="ch1">Chapter 1 · Project overview: 700 stars in two weeks—what is it actually selling</h2>

### 1.1 First look at a set of contrast numbers

| Metric | Value |
|---|---|
| Star | **733** (analysis day) |
| Fork | 90 |
| Repo age | **about 13 days** (2026-08-28 → 2026-09-10) |
| Python integration package | **768 lines / 9 files** |
| Web backend | **`web/app.py` single file 2,509 lines** |
| Frontend | **about 5,238 lines TS/TSX** |
| Skill | **112** (discover 9 / plan 16 / produce 50 / publish 20 / attribute 11 / general 6) |
| Shared scripts | **44** `skills/shared/scripts/*.py` |
| License | Apache-2.0 |
| Homepage | https://zju-real.github.io/Easel/ |

**The Python package is only 768 lines; Skills are 112.** That pair of numbers is itself an architectural statement:

> Easel’s product is not “write another Agent loop.” It is “give an already-there Agent the hands, rules, and workbench of the social-media domain.”

The most important sentence at the top of the README:

> Your private, continuously evolving social-media operations assistant. Starting from an idea, finish discover, plan, produce, publish, and review.

Six platforms are written into the capability table: Xiaohongshu, Douyin, Kuaishou, Zhihu, Bilibili, WeChat Channels. The research vision is blunt—take social intelligence from the lab and plug it into the piece of content a creator actually has to post every day.

### 1.2 What it explicitly is not

| Not | Is |
|---|---|
| another coding agent | a social-media content workbench |
| a homemade ReAct loop | OpenClaw’s **profile = `easel`** isolated workspace |
| a “feature-list” skill catalog | a skill library **with runnable scripts, artifact landing, and a real-publish gate** |
| one global USER.md / MEMORY.md | **a per-request self-contained persona prefix** + `memory.md` isolated by persona |
| a half-finished piece in a chat log | a project directory under `outputs/<human-readable topic>/` |

The README also writes two usage warnings that are worth reading as product constraints:

1. **The Web frontend is recommended**; CLI is only one of the entries.
2. **Be cautious about auto-publishing to Xiaohongshu**—the platform may detect automation; there is verification, rate limiting, risk control. Preview + a human confirm, then post.

### 1.3 Who is building it, and whom they thank

Zhejiang University REAL Lab + Peking University OpenDCAI. `docs/ACKNOWLEDGMENTS.md` lays open the sources of the 112 Skills: card templates explicitly credit **nexu-io/open-design**; de-AI-taste credits “speak like a human”; template reuse credits fabric……each Skill directory also has `EASEL-META.md` recording provenance. This is honest: **the skill library is compilation + original work, not a from-scratch invention of social-media methodology**. The engineering contribution is collecting them into **one contract** (frontmatter, outputs, gates, personas).

> 🧠 **One sentence**: Easel sells “a content partner that remembers your account.” In engineering it is **OpenClaw’s vertical host**. In this series, Open Design treats 25 CLIs as the engine for design; Easel treats **1** OpenClaw as the engine for social media.

---

<h2 id="ch2">Chapter 2 · Full stack: a thin integration layer + a borrowed engine + a skill library</h2>

### 2.1 Assembly diagram

```mermaid
flowchart TB
    subgraph UI["① What you can see"]
        WEB["React workbench<br/>chat / skill library / content library / accounts / personas<br/>trends / calendar / publish / review"]
        CLI["easel chat / skill / doctor"]
    end
    subgraph HOST["② Integration layer (what Easel wrote)"]
        PY["easel/ 768 lines<br/>persona · timeouts · CLI"]
        API["web/app.py 2509 lines<br/>SSE · login · publish · artifact tree"]
        SS["skills/shared/scripts 44<br/>output_paths · manifest · content_guard"]
    end
    subgraph ENGINE["③ Borrowed engine"]
        OC["openclaw --profile easel<br/>gateway :18789"]
        WS["~/.openclaw/workspace-easel/<br/>SOUL.md + AGENTS.md + skills copies"]
    end
    subgraph DISK["④ Project disk (the only truth)"]
        SK["skills/openclaw/ 112 SKILL"]
        PF["profiles/ six-dimension personas"]
        OUT["outputs/ content projects"]
        ENV[".env model and media keys"]
    end

    WEB --> API
    CLI --> PY
    API -->|subprocess + persona prefix| OC
    PY -->|subprocess + persona prefix| OC
    OC --> WS
    WS -.symlink.-> PF
    WS -.symlink.-> OUT
    OC -->|"cd to project root then run scripts"| SK
    SK --> SS
    SS --> OUT
```

### 2.2 Full stack

| Layer | Tech | Source location |
|---|---|---|
| CLI | argparse subcommands: `chat` / `doctor` / `gateway` / `ping` / `skill` / `web` | `easel/cli.py:124-171` |
| Persona | six-dimension md concatenation + message prefix + per-turn TURN_REMINDER | `easel/persona.py` |
| Timeouts | `TIMEOUT_PRODUCE=7200` / `TIMEOUT_DIRECT=300` / `TIMEOUT_CHAT=7200` | `easel/timeouts.py:11-13` |
| Web | FastAPI + SSE (sse-starlette) + CORS `*` | `web/app.py:190-191` |
| Frontend | React + Vite, 11 page components | `web/frontend/src/App.tsx`, `Sidebar.tsx:11` |
| Engine | global `openclaw` CLI, profile name hardcoded `"easel"` | `easel/cli.py:31`, `web/app.py:46` |
| Gateway | `127.0.0.1:18789/healthz` | `easel/commands/doctor.py:75-81` |
| Publish | Playwright Chromium + biliup + per-platform scripts | `pyproject.toml:20-21`, `web/app.py:1996-2053` |
| Media | Pillow / OpenCV / faster-whisper / edge-tts / rembg / librosa | `pyproject.toml:18-21` |
| Runtime | Python ≥3.10, Node ≥22.19, FFmpeg | `easel/commands/doctor.py:148-158` |

The CLI entry in `pyproject.toml:25-26` is one line: `easel = "easel.cli:main"`. Web is not an ASGI app inside the Python package; `easel web` then `subprocess`es `web/app.py` (`easel/cli.py:157-166`).

### 2.3 Rejected alternatives, read out of comments

The integration layer is extremely thin, but the comments are full of “we tried, we crashed, then we converged here”:

| Rejected | Why rejected | What they do now |
|---|---|---|
| Global `USER.md` writes the current persona | concurrent sessions overwrite each other | persona as **in-message inline** (`easel/persona.py:1-7`) |
| Global `MEMORY.md` carries account knowledge | cross-persona pollution | on sync, **empty** the workspace `MEMORY.md` (`openclaw/sync.sh:101-103`) |
| Three entries each write their own timeout | the same task can finish via CLI and get cut via Web | `easel/timeouts.py:1-8` |
| Three entries each write their own persona logic | behavior inconsistent | converge on `easel/persona.py` |
| `openclaw.json5` as the live config | setup never applies it; it would drift from real config | the file itself warns at the head (`openclaw/openclaw.json5:2-5`) |
| OpenClaw default `thinking=high` | when thinking blocks are stored into history they **drop the signature**; Bedrock replay 400 | default `low` + `session_heal` clean (`web/app.py:51-54`) |
| Rely only on `--session-key` to continue a session | idle ~24h binding expires; next-day history all gone | use uuid5 to **pin `--session-id`** (`web/app.py:823-833`) |

> 🧠 **One sentence**: Easel’s Python is short because it outsourced the loop; where it actually spent effort is **welding shut, one by one, the races, timeouts, session loss, and key leaks that necessarily appear after you outsource**.

---

<h2 id="ch3">Chapter 3 · Five-layer workflow: product narrative, not a runtime scheduler</h2>

### 3.1 Where the five layers are written

The product layer (README) and the Agent layer (AGENTS.md) use the same vocabulary:

| Layer | README wording | AGENTS.md wording | Skill `layer` field |
|---|---|---|---|
| Discover | trends and opportunities | trends, hits, remix opportunities | `discover` (9) |
| Plan | topics, titles, scripts, schedule | topics, scripts, storyboards, cover ideas | `plan` (16) |
| Produce / make | image-text, audio, video | any task that must create files, write into `outputs/` | `produce` (50) |
| Publish | check, adapt, really post | compliance, platform adapt, login, really post | `publish` (20) |
| Attribute | performance flows back into the persona | data, comments, Profile flow-back | `attribute` (11) |
| (cross-cut) | workbench basics | — | `general` (6) |

`openclaw/workspace/AGENTS.md:41` is restrained:

> Clear single-layer tasks execute directly; tasks that cross two or more layers or are obviously multi-step first give a short Plan. One task may compose multiple SKILLs, but do not run unrelated layers.

There is **no** Python class called `WorkflowEngine` that schedules by layer. The five layers are **division-of-labor rules written for the model**, landing on two things:

1. Each SKILL.md’s YAML `layer:` (routing and docs)
2. Across layers, `manifest.py` records “artifact path + one-sentence conclusion” in `outputs/<topic>/.easel.json` (`docs/SKILL-SPEC.md:136-149`)

### 3.2 A thin index for vertical orchestration

AGENTS.md’s rules for crossing layers (`openclaw/workspace/AGENTS.md:43-45`):

- Only build a manifest when crossing two or more layers; single-layer does not
- Register each layer complete **or failed**
- Downstream uses `latest` / `read` to take upstream, **does not re-derive, does not forward the whole block**
- Full payload writes to files; key decisions write `brief.md`

`docs/SKILL-SPEC.md:157-159` states the principle even harder: the manifest is **only a thin index**; `summary` one line for the orchestration layer to route; `outputs[]` points at paths; **does not copy content**.

This is the opposite philosophy of a coding agent stacking intermediate results in the conversation: **the conversation is the control plane; the disk is the data plane**.

```mermaid
sequenceDiagram
    participant U as User
    participant A as OpenClaw Agent
    participant M as manifest.py
    participant D as outputs/topic/
    U->>A: help me make a Xiaohongshu note and schedule publish
    A->>A: Plan: discover → plan → produce → publish
    A->>D: write brief.md / cards / copy
    A->>M: record --layer produce --outputs card_1.png,...
    A->>M: latest --layer produce
    M-->>A: path + one-sentence conclusion
    A->>D: publish script --exec
    A->>M: meta --status published
```

### 3.3 The special status of the produce layer

AGENTS.md defines “produce” as **any task that must create files** (`openclaw/workspace/AGENTS.md:31`), not merely “a pretty video.” Timeouts also budget by this definition: the chat entry uses the produce-layer 7200 seconds, because mid-conversation it may generate video (`easel/timeouts.py:7-13`).

The produce flow is a fixed five steps (`openclaw/workspace/AGENTS.md:53-63`): condense Profile → clarify spec → produce by SKILL → self-check → **rework once only on failure**. The produce layer **does not read** `platforms.md`; it obeys preferences red lines as-is. This is deliberate: when making content, first make the thing right; platform adaptation is left to the publish layer.

---

<h2 id="ch4">Chapter 4 · Three entries, one source of truth: chat / skill / web</h2>

### 4.1 Three entries, the same sentence

| Entry | How it enters OpenClaw | How persona is injected | Timeout |
|---|---|---|---|
| `easel chat` | `openclaw --profile easel chat --session ... --message <prefix>` | after picking a persona, the prefix is the **initial message** | `TIMEOUT_CHAT` (`easel/cli.py:103-117`) |
| `easel skill` | `openclaw agent --session-key skill-<ts> --message "please execute /<skill>…"` | `persona_prefix` concatenated before the instruction | always `TIMEOUT_PRODUCE` (`easel/commands/skill.py:156-164`) |
| Web `/api/chat/stream` | same `openclaw agent`, plus `--session-id` pinning the transcript | `chat_turn_message` = prefix + user original + TURN_REMINDER | `TIMEOUT_CHAT` (`web/app.py:1080-1084`) |
| Web `/api/skill` | `run_agent_sync` | `_persona_prefix` + `please execute /…` | `TIMEOUT_PRODUCE` (`web/app.py:1453-1463`) |

CLI `chat` **does not add** TURN_REMINDER; Web chat does. `easel/persona.py:72-76` explains why: a SKILL run alone need not; a conversation will be long, and the system prompt will decay.

### 4.2 What the persona prefix looks like

```text
我当前使用的画像是「科技数码达人」。本会话的账号长期记忆仅使用 profiles/科技数码达人/memory.md，不要使用工作区全局 MEMORY.md 作为账号记忆。
```

Implementation: `easel/persona.py:58-69`. Six-dimension file order is hardcoded (`identity / style / audience / platforms / preferences / memory`, `easel/persona.py:16-20`). `load_profile_text` concatenates in that order; other `.md` files are appended—but **what is injected into OpenClaw is not the full text**, only a pointer of “go read which directory.” The real condensing is required by AGENTS.md: the model itself reads `easel-profiles/<name>/`.

### 4.3 Per-turn behavior reminder: recency against instruction decay

`TURN_REMINDER` (`easel/persona.py:77-83`) is concatenated at the **end of the user message**, marked “internal reminder, do not restate.” Content is three sentences:

1. Before acting, look up the skill library; do not do it naked from memory
2. All five layers, you yourself write the finished product into `outputs/`
3. If asking about an account, first check login state; outbound copy must not leak keys, and must not self-disclose “AI generated”

The comment writes the motive clearly (`easel/persona.py:72-76`): AGENTS.md / SOUL.md are only fresh at the start of a session; later in a long conversation the model will “do it naked from memory.” Putting the most load-bearing reflex at the end costs almost nothing.

This is something rarely written as code in this series: **the system prompt will decay, so feed the load-bearing rules again, and feed them after the user’s words.**

### 4.4 How the skill entry digests input

`easel/commands/skill.py:48-66`: input may be text, or a path. Images are rewritten as “please process this image: absolute path”; audio/video/PDF/archives only pass the path, avoiding `read_text` exploding binary as UTF-8; ordinary text files are read into the message. Over-long strings (exceeding the filename cap) `Path.is_file` will throw `OSError`, treated as text—`tests/test_core.py:71-74` specifically locks this pit.

Skill names may omit the `skill-` prefix (`easel/commands/skill.py:39-45`); Web `find_skill` is the same logic (`web/app.py:213-219`).

---

<h2 id="ch5">Chapter 5 · Prompt four-layer stack: SOUL → AGENTS → CONTEXT → SKILL</h2>

`docs/prompt-stack.md` hardcodes the composition order. Contrast Open Design’s “20 layers banded by cache frequency”: Easel only splits four layers, and the **resident layer deliberately does not write concrete skill names**.

### 5.1 Layer 1 — SOUL.md (persona, 30 lines)

`openclaw/workspace/SOUL.md`: you are the creator’s “buddy”—you understand strategy and can also do the work. Capability overview writes “what you can do” by five layers, **does not write skill names**, to avoid going stale when a platform is taken down (`docs/prompt-stack.md:25`). Communication style: Chinese, give 2–3 options, outbound copy never expose tool traces.

### 5.2 Layer 2 — AGENTS.md (business constitution, 110 lines)

This is the most important file. Six core execution rules (`openclaw/workspace/AGENTS.md:5-12`):

1. **Route SKILL first**—exact match → nearest → only then use general capability
2. **Go to the project root first**—before the first script you must `cd` to the absolute path sync injected
3. **Do not run project copies in the workspace**—forbidden to run scripts from OpenClaw workspace’s `shared/`
4. **Look up existing information before asking**—login state, persona, historical artifacts first
5. **Paid operations confirm first**—image/video/music gen first give range and cost
6. **Only a real artifact counts as done**—plans, empty shells, prompts do not count

There is also a whole “config check” section: model keys can only be judged against the project-root `.env`; `env` / `printenv` cannot see values that were not exported; workspace `ls -a` also cannot see the project-root `.env`—neither can be used to claim missing config (`openclaw/workspace/AGENTS.md:16-23`). This was forced by a real crash: the Agent in the wrong cwd yelled “you didn’t configure a Key,” when the user actually had.

### 5.3 Layer 3 — CONTEXT.md (semi-static paths)

Generated by `sync.sh`, containing the project absolute path. Comments emphasize: **this claude version does not support `--cwd`**, so you must `cd` first then run scripts (`openclaw/sync.sh:128-137`).

### 5.4 Layer 4 — SKILL (on demand)

On trigger, load SKILL.md; references are read when cited; scripts **are called at execution time, code does not enter the prompt** (`docs/prompt-stack.md:36-39`). This is the same family of idea as Claude Code’s three-layer skill disclosure; Easel wrote it into its own SKILL-SPEC.

```mermaid
flowchart LR
    S["SOUL.md<br/>persona · category-level capability"] --> A["AGENTS.md<br/>division of labor · orchestration · safety"]
    A --> C["CONTEXT.md<br/>project-root absolute path"]
    C --> K["SKILL.md<br/>loaded only when triggered"]
    K --> R["references/<br/>domain knowledge on demand"]
    K --> SC["scripts/<br/>do not enter the prompt"]
    P["persona prefix<br/>every message"] -.-> A
    T["TURN_REMINDER<br/>only at the end of Web chat"] -.-> K
```

---

<h2 id="ch6">Chapter 6 · sync.sh: the workspace is the script; the project root is the worksite</h2>

### 6.1 The real path of the isolated profile

```
workspace → ~/.openclaw/workspace-easel/
config    → ~/.openclaw-easel/openclaw.json
```

(`openclaw/sync.sh:6-8`)

`--profile easel` cuts Easel off from any other OpenClaw workspaces that may exist on the user’s machine.

### 6.2 What sync does

1. **Delete SKILLs that no longer exist in the source** (`openclaw/sync.sh:30-38`)
2. **Whole-directory `cp -r` each SKILL** into workspace/skills (`openclaw/sync.sh:46-53`)
3. **Copy `skills/shared/`** into workspace/shared (for SKILL `../../shared/` relative refs)
4. **Copy AGENTS.md / SOUL.md**, then **append the “runtime project root” absolute path** (`openclaw/sync.sh:77-93`)
5. **Delete leftover USER.md**, **empty MEMORY.md** (`openclaw/sync.sh:97-103`)
6. **symlink** `easel-profiles` → project `profiles/`, `outputs` → project `outputs/` (`openclaw/sync.sh:106-124`)
7. Write `CONTEXT.md`

The symlink section wrote a real pit: you must `rm` first then `ln -s`, otherwise `ln -sf` follows the old link into the target directory and creates a cycle (`openclaw/sync.sh:107`).

### 6.3 Why scripts must run at the project root

`manifest.py:58-61` itself confesses: scripts are sync-**flattened and copied** to `workspace/shared/scripts/`, where `__file__` is one `skills/` layer short, walking up would compute `~/.openclaw`, and artifacts would write to the wrong place. So the **`EASEL_ROOT` env var is not optional**. CLI and Web both `setdefault("EASEL_ROOT", project root)` in `_proxy_env` (`easel/cli.py:36`, `web/app.py:303`).

The paragraph AGENTS.md appends after sync is equivalent to welding “where to `cd`” into the system prompt every turn. The `shared/` inside the workspace is only for the model to read docs; **it is not the execution cwd**.

### 6.4 openclaw.json5 is a template that will never take effect

The file head is unsparing (`openclaw/openclaw.json5:2-5`):

> This file is for reference only. Actual config is written by setup.sh via `openclaw config set` directly into `~/.openclaw-easel/openclaw.json`. This template is never applied by setup.sh.

Gateway port 18789, model `anthropic/claude-sonnet-4-6` are only illustrative. **A source reader who treats the json5 as runtime config will be wrong.** This is a rare honesty: mark a “template that may drift” as hazardous, rather than pretend it is the single source of truth.

---

<h2 id="ch7">Chapter 7 · Timeouts, proxy, doctor: make the environment a checkable product</h2>

### 7.1 Three timeout constants

```
TIMEOUT_PRODUCE = 7200   # generate video / multi-shot composite
TIMEOUT_DIRECT  = 300    # discover / plan / publish / attribute
TIMEOUT_CHAT    = TIMEOUT_PRODUCE
```

(`easel/timeouts.py:11-13`)

chat is budgeted at the produce layer because the user in conversation may anytime say “make it a video.” skill CLI always uses the produce-layer upper bound, simple and blunt, avoiding another layer-branch error.

### 7.2 Proxy: protect intranet direct connect

`_proxy_env` fills `EASEL_PROXY` into `http_proxy` / `https_proxy`, while `no_proxy` contains `localhost`, `127.0.0.1`, `*.xiaohongshu.com`, `*.devops.xiaohongshu.com`, `10.*` (`easel/cli.py:33-40`). This is a laboratory-intranet fingerprint: Xiaohongshu internal domains must connect directly; the outside goes through the proxy. `content_guard` later treats these internal domains as **BLOCK-level leaks**.

### 7.3 doctor checklist

`easel doctor` (`easel/commands/doctor.py:143-207`) ticks product dependencies one by one:

Python ≥3.10 / venv / Node ≥22.19 / FFmpeg / `openclaw` on PATH / fastapi·uvicorn·sse_starlette·multipart / frontend `dist/index.html` / Playwright Chromium / any auth channel in `.env` / gateway `:18789/healthz` / `~/.openclaw/workspace-easel/skills/` nonempty / key files exist.

Auth channels are “any one is enough” (`easel/commands/doctor.py:92-140`): `ANTHROPIC_API_KEY`, or `EASEL_LLM_API_KEY + BASE_URL`, or `ANTHROPIC_AUTH_TOKEN + BASE_URL`, or `OPENAI_API_KEY`, or Volcano MaaS. Placeholder `REPLACE_ME` does not count as configured.

`easel ping` then does a real connectivity check: healthz + make the agent say PONG (`easel/commands/ping.py:48-78`). doctor is static existence; ping is the authority.

---

<h2 id="ch8">Chapter 8 · SKILL-SPEC: three-layer load and a 200-line cap</h2>

`docs/SKILL-SPEC.md` is the skill-library interface spec v0.3.

### 8.1 Directory and three layers

```
skills/
├── openclaw/     five-layer SKILL, OpenClaw executes directly
└── shared/       cross-SKILL scripts and config
skill-xxx/
├── SKILL.md      required, < 200 lines, only write “how to do it”
├── references/   domain knowledge, load on demand
├── scripts/      called at runtime, code does not enter the prompt
└── tests/        test1.prompt / test1.expected
```

| Layer | Load timing | tokens |
|---|---|---|
| Metadata (name, description, layer) | resident, for routing | tiny |
| Instructions (SKILL.md body) | when triggered | medium |
| Resources (references + scripts) | on demand during execution | on demand |

frontmatter **keeps only three regular fields**: `name` / `description` / `layer`. Forbidden: `version`, `allowed-tools`, `tags`, etc. (`docs/SKILL-SPEC.md:73-78`). `description` must in Chinese write capability, trigger wording, and the boundary with neighboring SKILLs.

Two validation commands: `scripts/validate_skills.py` (frontmatter, resource links, output and publish safety contracts) and `scripts/validate_skill_commands.py` (Python commands in SKILL vs script argparse, against path drift).

### 8.2 Five design constraints

Independently tunable, usable without a Profile, stable interface, SKILL.md lean, generalize without giving a concrete case (`docs/SKILL-SPEC.md:167-173`).

“Usable without a Profile” decides that the product can be played with first, personas built slowly; “independently tunable” decides that 112 directories can be changed in parallel, rather than one big net.

### 8.3 How it differs from a coding agent’s skill

Claude Code / Codex skills are mostly “teach the model how to use a tool.” Easel’s skill **itself is a production pipeline**: SKILL.md writes which `python skills/.../scripts/foo.py` to run; artifact paths are gated by `output_paths.py`; before publish they are scanned by `content_guard.py`. A Skill is not a manual; it is an **SOP with an executor**.

---

<h2 id="ch9">Chapter 9 · 112 Skills: read the library by layer</h2>

Counts follow `docs/skill-function-mapping.md` and the checked-out directories; frontmatter `layer` stats:

| Layer | Count | What this layer is doing |
|---|---:|---|
| general | 6 | artifact management, batch processing, login-account query, persona build/manage, template library |
| discover | 9 | hot search, competitors, content gaps, cross-platform differences, holidays, industry news, RSS, algorithm updates, UGC |
| plan | 16 | positioning, audience, persona, topic matrix, scoring, calendar, hooks, outline, storyboard, livestream, brand deals |
| produce | **50** | text / visual / audio / video / novel / short drama / paper explainer—half the house |
| publish | 20 | six-platform upload, cross-platform distribution, schedule, quality gate, comment reply, short links, notifications |
| attribute | 11 | data, comment insight, ROI, review, persona memory |

The produce layer’s 50 is not “50 kinds of copy.” It collects ffmpeg, TTS, image gen, video gen, subtitles, green screen, beat-sync, albums……into atoms the Agent can call. The shared-script layer is the real multimedia SDK.

Web-side `get_skills()` (`web/app.py:270-286`) scans `skills/openclaw/*/SKILL.md`, parses description/layer, and marks a few Skills `needsApi` (image/video/music gen / clone / short drama / paper explainer, `web/app.py:142-170`). The frontend skill-library page uses this table to decide whether to show an exclamation “go configure a Key.”

`SKILL_API_REQUIREMENTS` and `skills/shared/scripts/model_registry.py` share the same truth—`tests/test_core.py:33-39` asserts the Web’s provider list equals the registry, and serialized results must not contain keys.

### 9.1 A few produce-layer types worth naming separately

| Type | Representative Skill | Trait |
|---|---|---|
| Platform total entry | `xhs-note-creator` | Xiaohongshu image-text/video SOP, forced de-AI, goes through card-design |
| One-click film | `auto-short-video` | copy→stills/AI video→voiceover→subtitles→BGM→composite |
| Long content | `novel-writer` / `paper-explainer` / `short-drama` | file-ized state, cross-chapter consistency, series bible |
| Deterministic media | `image-editing` / `video-editing` / `audio-editing` | not the model; OpenCV/ffmpeg |
| De-AI taste | `text-polisher` | seven-pass scan + Chinese AI-marker table, cited by other Skills as the authority |

`xhs-note-creator`’s SKILL.md is explicit: a whole note uses this SKILL; card render only uses `card-xiaohongshu`; other platforms use `social-content` (`skills/openclaw/xhs-note-creator/SKILL.md:4-6`). **The boundary is written in the description**—that is the entire routing mechanism. There is no Python router.

---

<h2 id="ch10">Chapter 10 · outputs contract: content is a project, not a chat log</h2>

### 10.1 Directory convention

```
outputs/<human-readable topic>/
├── note.md / final.mp4 / card_1.png   finished product, at the project root
├── assets/                            intermediates: frames, slices, drafts
└── .easel.json                        display header + steps[] (hidden)
```

Forbidden: generic names (`xhs` / `test` / `tmp` / `主题`……, `output_paths.py:25-29`); finished products scattered at the `outputs/` root; content written into `_` system directories. System-directory allowlist: `_analytics _debug _inbox _login _probe _profile_build _publish _scratch _sessions` (`output_paths.py:30-33`).

`validate_output_path` (`output_paths.py:41-75`) is the gate of every new script: resolve to the project root, must be under `outputs/`, content at least two path levels, generic names throw `OutputPathError` outright. System writes must `allow_system=True` and top in the allowlist.

### 10.2 Two jobs of the manifest

`skills/shared/scripts/manifest.py`:

1. **Display header** (`meta`): title / platform / kind / status / tags / cover / deliverables—for the frontend content library as cards
2. **steps[]** (`record`): layer / skill / at / status(done|failed) / outputs[] / upstream[] / summary—for downstream orchestration

`kind` ∈ article / xhs-note / video / cards / poster / audio / other; project `status` ∈ draft / ready / published. `atomic_write` uses mkstemp + `os.replace` (`manifest.py:107-118`).

Web `_read_project_meta` (`web/app.py:505-520`) only takes display fields; does not leak steps to the content-library UI. Cover resolve: declared cover → first finished media → first image/video in the directory.

### 10.3 Attachment isolation

User uploads go into `outputs/_inbox/`, isolated by session. This turn’s message attaches a “system attachment list”; **only paths in the list may be used**; scanning other inbox files is forbidden (`openclaw/workspace/AGENTS.md:91`, `web/app.py:790-795`). When taken into a project, copy to `outputs/<project>/assets/`; the inbox original is kept for retry.

This is a threat model unique to publish-class Agents: the inbox may have material another session just uploaded; if the model `ls`s once, it contaminates.

---

<h2 id="ch11">Chapter 11 · Showcase: how xhs-note-creator makes a note</h2>

Xiaohongshu is Easel’s home field. This SKILL defines a “note” as **3–9 3:4 cards or a 15–90 second vertical storyboard**; long text is only an intermediate (`skills/openclaw/xhs-note-creator/SKILL.md:14-19`).

Workflow (steps cannot be skipped):

0. Intake: topic / form / material / style—ask once
0.5 Selling-point formula: scarcity × usefulness × perceivability
1. If there is material, `analyze_material.py` produces a list
2. Opinion pieces must have external references; core data ≥2 sources
3. Write a 2000–4000 character long-form manuscript, **confirm with the user first**
4. **Forced de-AI**—rules are not maintained as a copy in this SKILL; uniformly go through `text-polisher`’s references (`SKILL.md:73-79`)
5. Visuals go through `card-design` nine styles; render goes through `card-xiaohongshu`
6. QA, land `outputs/`, register the manifest

Among the five hit principles there is a load-bearing sentence: **visuals cannot be crude**. Wanting “amateur/hand-account” still picks card-design’s sticker style, rather than actually using crude t2i plus large emoji (`SKILL.md:25`). This is Open Design’s “anti-AI-taste” landing on social-media cards; ACKNOWLEDGMENTS also admits the card templates borrowed open-design.

The scripts directory has `validate_meta.py`, `analyze_material.py`, `normalize_slug.py`, `crop_watermark.py`, `text_on_image.py`, `collage_3x4.py`—the model owns judgment and copy; pixel-level operations go to deterministic scripts.

---

<h2 id="ch12">Chapter 12 · Two gates: content_guard hard-blocks; persona_gate only reminds</h2>

This is one of Easel’s most copy-worthy product decisions: **an Agent that will really post to a public platform must assume the model will write internal settings into the copy.**

### 12.1 content_guard: fail-closed, exit code 7

The head of `skills/shared/scripts/content_guard.py` writes the accident (`:3-7`): in the past there was no filter between generation and `--exec`; keys, internal URLs, proxy IPs, “generated by AI,” once posted, cannot be deleted.

Two forced levels (`content_guard.py:97-105`):

| Level | Category | On real publish |
|---|---|---|
| **BLOCK** | api-key / env-value / internal-host / proxy-ip / internal-path / env-name | **exit 7**, rewrite and resend |
| **WARN** | ai-disclosure / model-name | warn only. In a paper explainer, “Claude” may be body text |

Scan is dual-path: a regex table + **literal values of sensitive keys in `.env`** (`content_guard.py:141-176`). Hit fragments are masked in the report (`_mask` / `_snippet`), avoiding a second leak via logs. dry-run all only warn; releasing a hard block requires explicit `--allow-unsafe`. AGENTS.md requires: **unless the user explicitly asks, do not use this switch to bypass** (`openclaw/workspace/AGENTS.md:72`).

In the regexes you can see lab topology: `maas.devops.xiaohongshu.com`, `webide-gateway.devops.xiaohongshu.com`, `/mnt/tidal-alsh01`, `happyhorse` internal video-gen name (`content_guard.py:50-85`). The gate is both a general product and a desensitization layer when this code was open-sourced from an internal tool.

### 12.2 persona_gate: persona check never blocks publish

Before a publish-layer SKILL executes, if there is a Profile, run `skill-persona-check` (LLM scores), then hand to `persona_gate.py check` to turn the score into pass/warn. Threshold 80 (`persona_gate.py:29`). **Exit code is always 0** (`persona_gate.py:10-11, 54`): `publish_allowed: true`.

AGENTS.md nails the reason (`openclaw/workspace/AGENTS.md:74-78`): persona check only reminds; if the user already explicitly wants to post, continue, do not extra-ask for confirmation. Content safety and platform compliance still independently hard-block.

`record` writes the score into the manifest’s publish step, for later audit.

> 🧠 **One sentence**: a key leak is an irreversible public accident, so hard-block; “this piece does not quite fit the persona” is a creative judgment, so only remind. The two gates’ force is inverted—the correct split of responsibility for “automated publish.”

```mermaid
flowchart TD
    C[copy to publish] --> G{content_guard}
    G -->|BLOCK hit| X[exit 7 rewrite]
    G -->|pass or WARN only| P{has persona?}
    P -->|yes| S[skill-persona-check LLM score]
    S --> PG[persona_gate classify]
    PG -->|pass / warn both continue| E[--exec real publish]
    P -->|no| E
```

---

<h2 id="ch13">Chapter 13 · Login and one-click publish: Playwright + SMS wall</h2>

### 13.1 Six-platform login backend

`LOGIN_RUNNERS` (`web/app.py:98-105`):

| Frontend key | Display name | backend |
|---|---|---|
| xiaohongshu | Xiaohongshu | `xhs` |
| douyin | Douyin | `douyin` |
| kuaishou | Kuaishou | `web` + `wp=kuaishou` |
| weixin-channels | WeChat Channels | `web` + `wp=weixin-channels` |
| zhihu | Zhihu | `web` + `wp=zhihu` |
| bilibili | Bilibili | `biliup` |

Browser profiles live in `~/.easel-browser-profiles` (`web/app.py:82`). Login state writes `outputs/_login/`. whoami real verify has to start a headless browser; in-process cache 600 seconds (`web/app.py:93-96`), avoiding the accounts page and the workbench opening browsers twice.

### 13.2 `/api/publish/{platform}`

Second confirmation is on the frontend. The backend assembles the `--exec` command (`web/app.py:1996-2053`):

- Xiaohongshu: `xhs_publish.py publish` or `publish-video`
- Douyin: same structure, but **async**—may trigger an SMS wall; state writes `outputs/_publish/douyin.json`; verification code writes `douyin.code`; frontend polls until `sms_required` popup
- Bilibili: direct `biliup upload`, default partition tid=36 “Knowledge,” no-tag fallback “Daily”
- Others: `web_publisher.py --platform <wp>`

Constraints: some platforms must carry media; image and video cannot post together; Channels-class can only post video. Timeout 600 seconds. Each publish appends `outputs/_publish.log`.

Publish goes through `_publish_env()`, separate from the chat proxy environment, avoiding hitting the platform domain with a proxy and losing login state.

### 13.3 Why the README tells you to be careful with Xiaohongshu

Automated operations will be risk-controlled. Easel chose “can really post” rather than “only give copy”—this is product differentiation, and also the cost of compliance and account safety. The code stacks dry-run, preview, human confirm, content_guard in front of `--exec`, but **cannot stop platform-side behavior detection**. This is not a bug. It is a warning that must be written on the packaging after the host chose to enter the real world.

---

<h2 id="ch14">Chapter 14 · SSE chat: disconnect does not kill, dual lock, uuid5, thinking self-heal</h2>

The chat implementation in `web/app.py` is the most “systems” stretch of this thin integration layer. Comment density is extremely high; almost every one corresponds to an online accident.

### 14.1 Problem 1: CLI does not stream

`openclaw agent` buffers model output until the end then prints (`web/app.py:1028-1032`). Answer: set `OPENCLAW_RAW_STREAM=1` + a per-turn independent jsonl path; the backend tails the file, turns `assistant_text_stream`’s `text_delta` into SSE `token`, thinking into `thinking`. Per-turn independent files, no concurrent crosstalk.

### 14.2 Problem 2: a proxy will cut SSE, but produce has to run a long time

supervisor (runs openclaw) and forward (to the browser) are split (`web/app.py:1037-1039, 1352-1374`):

- Client disconnect **only ends forward**, does not cancel supervisor
- Full result lands `outputs/_sessions/<sk>.json`; frontend takes it back with `/api/chat/last`
- Also `/api/chat/jobs/{turn_id}/stream` continues SSE from the jsonl (`web/app.py:986-1021`)
- **Only the user clicking stop** `terminate`s the process (`web/app.py:1408-1415`); disconnect does not kill

A long task when a WebIDE proxy cuts SSE is the scene named in the comments (`web/app.py:946-947`).

### 14.3 Problem 3: two openclaw on the same session = takeover crash

OpenClaw concurrent write of the same session throws `EmbeddedAttemptSessionTakeoverError`; the symptom is “stops at a colon halfway through the answer” (`web/app.py:808-811`). Dual-layer lock:

1. **In-process** `asyncio.Lock` by session-key (`web/app.py:812-820`)
2. **Cross-process** `fcntl.flock` (Windows uses `msvcrt.locking`) (`web/app.py:841-896`)

Different sessions can still run in parallel. If flock cannot be taken, return “running in another window.” The frontend also has BroadcastChannel probing cross-tab occupancy (Chapter 15).

### 14.4 Problem 4: ask again the next day and it has forgotten

OpenClaw relies on `--session-key` to resolve the transcript; idle beyond about 24h (`threadBindings.idleHours`) the binding expires, and a new empty transcript is started (`web/app.py:823-827`). Answer: uuid5 with a fixed namespace, **the same web sessionId is always the same `--session-id`** (`web/app.py:828-833`), bypassing key→binding expiry.

### 14.5 Problem 5: thinking blocks drop the signature, replay 400

When OpenClaw stores Claude thinking into jsonl it drops the signature; intranet Bedrock replay verification fails (`scripts/session_heal.py:3-6`, `web/app.py:51-54`). Before every spawn, `_heal_openclaw_session` deletes thinking / redacted_thinking blocks and empty messages. Default `EASEL_THINKING_LEVEL=low` reduces production.

### 14.6 Problem 6: “it finished answering” but it did not actually finish

Finish detection (`web/app.py:1267-1290`) does not treat “already spat words” as success. The **only mark** of a normal finish is that the last event of the raw stream is `assistant_message_end`. Otherwise append a warning for the user to see: hit-ceiling truncation, process killed, stopped at `tool_use`, stream cut, **body stopped at a Chinese colon** (user-measured: “every mysterious stop stops at a colon”).

Every finish also writes one diagnostic line to `outputs/_debug/chat-stream.jsonl` (`web/app.py:1313-1333`): rc, stop_reason, last_ev, token/thinking character counts, whether it mistakenly received another session’s raw events.

```mermaid
flowchart TB
    B[browser] -->|POST /api/chat/stream| F[forward generator]
    F -->|SSE token/thinking/activity| B
    S[supervisor background task] -->|client_q| F
    S --> L[asyncio lock + flock]
    L --> P[openclaw agent subprocess]
    P --> RAW[per-turn jsonl raw stream]
    S -->|tail| RAW
    S -->|atomic write| T[outputs/_sessions/sk.json]
    B -.disconnect.-> F
    S -.continue after disconnect.-> T
    B -->|/api/chat/last| T
```

---

<h2 id="ch15">Chapter 15 · Frontend workbench: 11-page SPA and cross-tab sessions</h2>

### 15.1 Pages

`Page` type in `Sidebar.tsx:11`:

`dashboard | chat | trends | ideas | calendar | publish | breakdown | skills | outputs | accounts | profile`

The sidebar only puts six primary navs (workbench / chat / skill library / content library / accounts / personas); trends, topics, calendar, publish, review fold into workbench SubNav, avoiding sidebar explosion (`Sidebar.tsx:31-39`).

`App.tsx` puts **streaming state on the root component**; switching pages does not unmount, does not drop the stream (`web/frontend/src/App.tsx:164` comment). This is the correct structure for “the user goes to the content library to glance at a generated artifact while the conversation is still running.”

### 15.2 Cross-tab: BroadcastChannel + flock fallback

Mount logic (`App.tsx:55-125`):

1. Same-tab refresh: `sessionStorage` remembers this tab’s session → continue
2. New tab: use BroadcastChannel to ask “who is using the last active session”; if someone answers `owned`, open a new session, avoiding two windows colliding on the same OpenClaw session
3. No BroadcastChannel support: fall back to old logic; backend flock can still block the crash

Onboarding: no persona at all and has not seen the guide → recommend the config wizard (`App.tsx:144-147`). Also handled a brand rename: old localStorage key migrated from `postcraft_onboarding_seen` to `easel_onboarding_seen` (uses `['post','craft'].join('')` to dodge simple literal scans, `App.tsx:36`).

### 15.3 API client

`web/frontend/src/lib/api.ts` derives `BASE` from the current pathname, convenient for subpath deploy. Errors prefer FastAPI `detail`. Types write the `.easel.json` display header and artifact-tree nodes in full, aligned with backend `_read_project_meta` / `_build_output_node`.

---

<h2 id="ch16">Chapter 16 · What is actually new</h2>

In this series’ coordinate system, Easel is not “another stronger loop.” It is new in five things:

### 16.1 A vertical host, and the host only plugs into one engine

Open Design’s host plugs into 25 CLIs; the adapter is data. Easel only plugs into OpenClaw, and puts all differentiation into **SKILL + persona + real-publish gates**. Narrower, so it can take social-media SOP deep (112, with scripts, with a contract).

### 16.2 Conversation is the control plane; the project directory is the data plane

A coding agent’s artifacts often live in diffs and chat. Easel forces `outputs/<topic>/` + finished/intermediate split + a thin manifest. This is “one topic, one folder” that a content team can understand, not an engineer’s session log.

### 16.3 Persona is part of the request, not a global file

After rejecting USER.md, concurrent sessions no longer step on each other. Memory is isolated by `profiles/<name>/memory.md`; global MEMORY.md is kept empty by the sync script. This is the correct memory model for multi-account operations.

### 16.4 Graded gates before real publish

content_guard’s BLOCK/WARN grading, persona_gate never blocking, `.env` literal-value scan—aimed at the accident unique to the publish scene: “the Agent will say the internal world out loud.” A coding agent’s sandbox does not solve this.

### 16.5 Treat OpenClaw’s pits as a product surface to fix

thinking dropping the signature, 24h session-binding expiry, concurrent takeover, SSE cut by a proxy, instruction decay, wrong cwd causing a false missing-Key report—none of these automatically get better by “calling the CLI.” A large half of Easel’s 2509-line backend is **adapter-style wound dressing for the upstream engine**. This is the same class of work as Open Design writing parsers for 25 CLIs.

---

<h2 id="ch17">Chapter 17 · Engineering quality: highlights and hard wounds</h2>

### 17.1 Highlights

| Point | Evidence |
|---|---|
| Three entries converge on one source of truth | `persona.py`, `timeouts.py`; comments write the historical fork |
| Deterministic scripts with selftest | content_guard / persona_gate / manifest / output_paths |
| Atomic write | manifest `os.replace`; turn result `.tmp` then replace |
| Tests lock the contract | `tests/test_core.py` covers input classify, path safety, registry desensitization, persona_gate |
| Skill provenance traceable | ACKNOWLEDGMENTS + each Skill’s EASEL-META.md |
| Config template marked invalid | `openclaw.json5` head warning |
| Diagnostic logs | `chat-stream.jsonl` classifies “mysterious stop” |

### 17.2 Hard wounds and costs

1. **`web/app.py` single file 2509 lines.** Routes, locks, SSE, login, publish, artifact tree, persona CRUD all squeezed together. Understandable for a two-week repo; unfriendly to later contributors.
2. **Version-number split.** `pyproject.toml:7`’s `0.1.0` vs `easel/__init__.py:3`’s `0.0.1`.
3. **The engine is not in the repo.** OpenClaw is `npm i -g openclaw`. Analysis cannot check the loop itself; Easel’s correctness depends on upstream CLI flags (`--session-id`, `OPENCLAW_RAW_STREAM`) continuing to exist.
4. **json5 template drifting from real config** is known, but newcomers will still be fooled by the filename.
5. **Skill quality is necessarily uneven.** 112 directories; some are complete pipelines (xhs-note-creator); some are closer to prompt wraps. `validate_skills.py` can lock format; it cannot lock “whether this SOP is useful in real operations.”
6. **Tests are only 650 lines**, and explicitly only test integration-layer pure functions (`tests/test_core.py:1-4`). Publish scripts, Playwright, OpenClaw interaction are a test blind spot—the most dangerous part of the real-publish path is the hardest to test.
7. **CORS `allow_origins=["*"]`** (`web/app.py:191`) is enough for a local workbench; it is not a shape that can be exposed to the public net.
8. **Xiaohongshu auto-publish risk control** is written into the README, but the code path still provides `--exec`. The product hands the choice to the user, and also the risk.
9. **sync is whole-directory copy**, not incremental. 112 Skills full `cp` every time; workspace and source may be overwritten while the Agent is running.
10. **TURN_REMINDER appended every turn**; long-conversation token cost grows linearly; the exchange is routing hit rate. This is an open trade-off (`easel/persona.py:72-76`).

---

<h2 id="ch18">Chapter 18 · Twelve lessons for building a new product</h2>

1. **First decide who owns the loop.** If domain SOP is worth more than general reasoning, do not write the loop; write a host.
2. **The host’s core work is dressing the engine’s wounds**, not one `subprocess.run` line.
3. **Treat domain artifacts as first-class citizens of the filesystem**; do not let them live in the chat log.
4. **Cross-step transfer uses a thin index + paths, not orally passing the full text.**
5. **Text that will go to the public net must have a deterministic scan.** “Do not leak” in the prompt is not enough.
6. **Grade the gates: irreversible accidents hard-block; aesthetics/persona only remind.**
7. **Multi-tenant memory does not live in a global file.** Persona is part of the request.
8. **A long conversation must assume the system prompt will decay**; feed load-bearing rules again via recency.
9. **Decouple SSE from long tasks: disconnect does not kill; results land on disk; the frontend comes to take them.**
10. **The same session at the same moment allows only one engine process**—the lock must go outside the process.
11. **Make environment checks a product command** (doctor / ping); do not only write a README.
12. **Write “this file does not take effect” at the file head.** A drifting template is more dangerous than no template.

---

<h2 id="ch19">Chapter 19 · Its place in this series</h2>

```mermaid
flowchart LR
    subgraph LOOP["write the loop themselves"]
        CC["Claude Code / Codex / dsh<br/>general: write code, do work"]
        MF["MiroFish<br/>vertical: simulation rehearsal"]
    end
    subgraph HOST["do not write the loop · host"]
        OD["Open Design<br/>vertical: design files"]
        EA["Easel<br/>vertical: social-media content + real publish"]
    end
    CC -.skill idea.-> EA
    OD -.card visuals / host shape.-> EA
```

| Project | Question | Relation to Easel |
|---|---|---|
| Open Design | do not write the loop; plug into 25 CLIs for design | **nearest kin**: both hosts. Easel only plugs into 1 engine; the skill library is more vertical; card visuals also borrowed it |
| OpenWorker | general-task coworker, inbox + approval | both face “non-coding work”; OW emphasizes recovery when the human is away; Easel emphasizes content projects and real publish |
| MiroFish | simulation, does not do work | both non-coding; one rehearses a world, one produces content |
| Claude Code / Codex | coding agents that write the loop | Easel uses that kind of skill idea on social-media SOP; the loop itself is outsourced |
| DeepSeek Harness | the loop is also a plugin | contrast: dsh takes replaceability to the extreme; Easel gives replaceability to upstream and itself locks the domain contract |

**Lineage patch**: the first seven coding agents → OpenWorker coworker → Open Design design host → MiroFish simulation → **Easel social-media host**. It takes the “do not write the main loop” route from a design tool into the six platforms a domestic creator faces every day.

Companion reading: [Build a social-media content workbench from scratch (Easel lineage)](../从零构建Easel-开发全流程教程.html) · interactive benches [Easel-解析.html](../Easel-解析.html)

---

<h2 id="ch20">Chapter 20 · 🔍 Source pointer table</h2>

All against commit `765f5a6`.

| You want to see | Go here |
|---|---|
| CLI entry and chat injecting persona | `easel/cli.py:49-121` |
| Three-entry persona prefix / TURN_REMINDER | `easel/persona.py:1-99` |
| Timeout constants | `easel/timeouts.py` |
| skill routing and binary input | `easel/commands/skill.py:31-164` |
| doctor / ping / gateway | `easel/commands/doctor.py`, `ping.py`, `gateway.py` |
| Prompt-stack explanation | `docs/prompt-stack.md` |
| SKILL spec | `docs/SKILL-SPEC.md` |
| 112 Skill capability map | `docs/skill-function-mapping.md` |
| Agent constitution | `openclaw/workspace/AGENTS.md` |
| Persona | `openclaw/workspace/SOUL.md` |
| Sync and symlink | `openclaw/sync.sh` |
| “this config does not take effect” | `openclaw/openclaw.json5:1-35` |
| SSE supervisor / dual lock / uuid5 | `web/app.py:808-1401` |
| Stop vs disconnect | `web/app.py:1408-1415` |
| Skill HTTP | `web/app.py:1453-1463` |
| Artifact tree and display header | `web/app.py:488-545` |
| Login table | `web/app.py:98-105` |
| One-click publish | `web/app.py:1996-2068` |
| Frontend cross-tab | `web/frontend/src/App.tsx:55-125` |
| Page enumeration | `web/frontend/src/components/Sidebar.tsx:11-39` |
| Path gate | `skills/shared/scripts/output_paths.py` |
| Cross-layer contract | `skills/shared/scripts/manifest.py` |
| Outbound scan | `skills/shared/scripts/content_guard.py` |
| Persona reminder | `skills/shared/scripts/persona_gate.py` |
| thinking self-heal | `scripts/session_heal.py` |
| Xiaohongshu SOP | `skills/openclaw/xhs-note-creator/SKILL.md` |
| Dependencies and version | `pyproject.toml` |
| Integration-layer tests | `tests/test_core.py` |
| Skill acknowledgments | `docs/ACKNOWLEDGMENTS.md` |
