docs: refresh top-level README + add dsh-arch-diagram skill

Replaces the 20-line stub with a bilingual README pair covering:
- getting started (`pnpm run demo:repl` + Node ^22.19 || ≥24 + pnpm 11.7)
- editor integration (Zed / VS Code / Cursor over ACP)
- programmatic embedding via `@deepseek-ai/dsh-app-boot` (with the
  two failure paths of `boot()` vs `installFailLoud` spelled out)
- writing a plugin (`echo-tool` grounded in `examples/echo-agent`, with
  the runnable command `pnpm run demo:echo`)
- Packages table grouped by family, including `session-query`,
  `context`, and Support (with `invariants` called out as a runtime
  diagnostic mounted by `dsh-agent-core`, not test-only)
- deep-dive links, community, license

Bilingual pairing: English and Chinese sides share byte-identical code
blocks and mirroring link ordinals; language-asymmetric community
channels (Discord/X in EN, WeCom in ZH) sit inside HTML `<a href>`
tags that the pairing gate's structural signature excludes by design.
ZH side follows `docs/i18n/terminology.md` — first `agent` occurrence
annotated as `agent(智能体)`, plain `agent` thereafter.

Ships an in-repo skill at `.agents/skills/dsh-arch-diagram/` that owns
the two architecture PNGs (`assets/arch-{en,zh}.png`). HTML templates
+ shared CSS + a `render.sh` that renders via Chrome headless. The
renderer is portable (auto-detects Chrome/Chromium on macOS + Linux,
uses `--no-sandbox` under root for container envs, discovers the local
http.server port from Python's own startup line so `lsof` isn't
required). The 12-card capability row uses `flex: 0 0 auto` so
adjacent card backgrounds cannot clip descriptions; layout tuned to
1560px page / 96% row-width / 1820×580 render viewport.

Consolidates the four review-response commits from the initial round
of ds-review-bot feedback; each review round is documented in a
top-level PR comment for traceability.
This commit is contained in:
NI0317
2026-07-15 20:57:51 +08:00
parent b045b553a9
commit 069e0ecdc9
12 changed files with 1069 additions and 22 deletions

View File

@@ -0,0 +1,158 @@
---
name: dsh-arch-diagram
description: Regenerate the DeepSeek Harness "System overview" architecture PNGs (English + Chinese) that the top-level README embeds. Reflects the current state of packages/* and docs/architecture.md. Use when the codebase adds/removes/renames services and the README diagram must be refreshed.
---
# dsh-arch-diagram
Produces `assets/arch-en.png` and `assets/arch-zh.png` in this repo — the two hero images embedded at the top of `README.md` and `README.zh.md`.
The pipeline is HTML + Chrome headless, so text quality, layout, and connector geometry are **deterministic** — every rerun produces the same output regardless of model.
Aesthetic is locked to the DeepSeek brand: light blue gradient background, brand-blue Cordis bar, sans-serif everywhere, no serif / no italic / no hand-drawn feel.
---
## When to use
- User asks to "regenerate" or "refresh" the Harness architecture diagram
- User points out `packages/` has changed (new services, renames) and the README diagram is stale
- User just added a new `packages/<family>/` or a new `ctx.*` service and wants the diagram to reflect it
## When NOT to use
- User wants a different diagram (Code Mode, Workflow fanout, agent lifecycle) — those belong in `docs/` and need their own skill
- User wants a different visual aesthetic (dark mode, hand-drawn, editorial-serif) — this skill is locked to DeepSeek brand
---
## Workflow
### 1. Enumerate current services from the repo
The diagram is grounded in two lists that live in the docs:
- **Default Services** — from `packages/core/*` and `docs/architecture.md`'s "Default Services" table. As of last refresh: `ctx.sessions`, `ctx.systemPrompt`, `ctx.tools`, `ctx.agents`, `ctx.agentLoop`.
- **Capability Services** — from the non-core capability packages and `docs/architecture.md`'s "Capability Services" table. As of last refresh: `ctx.llm`, `ctx.bash`, `ctx.sandbox`, `ctx.codeRuntime`, `ctx.fs`, `ctx.skills`, `ctx.web`, `ctx.compact`, `ctx.subagents`, `ctx.workflows`, `ctx.sessionPersistence`, `ctx.sessionQuery`.
**Do this**:
1. Read `docs/architecture.md`
2. Extract the two `ctx.*` tables verbatim
3. Cross-check against `packages/`: services live under `packages/core/*` (defaults) or under `packages/<family>/<package>/` (capabilities). The inner package dir mirrors the npm name without the `@deepseek-ai/dsh-` prefix — e.g. `packages/session-query/session-query/` publishes `@deepseek-ai/dsh-session-query`. Never assume the inner dir carries a `dsh-` prefix.
### 2. Diff against the current HTML templates
Read `harness-arch-en.html` and `harness-arch-zh.html` in this skill directory. Pull out the currently-rendered `ctx.*` names. Report the diff:
```
Default services: no change
Capability services: + ctx.<new>, − ctx.<removed>, ↻ ctx.<renamed>
```
### 3. Update the templates
If there are changes, edit both HTML files in place. Each card is one line:
```html
<div class="card"><span class="name">ctx.something</span><span class="desc">short description</span></div>
```
For a **new** service:
- Description is **short** — 2–4 words (EN), 3–6 chars (ZH)
- Ground the description in what the package's README or its `ctx.<name>.register()` **actually** does. Do NOT paraphrase from an abstract or invent capabilities the code doesn't have.
- Add the card in **both** languages, in docs order.
For a **removed** service: delete the card in both files.
For a **renamed** service: update the `.name` span in both files.
**Do not touch the layout**. Rows are:
- Top row (`row top`): exactly 5 default-service cards
- Bottom row (`row bottom`): 12 capability-service cards
If the capability count grows past ~13, the row gets visually tight. Stop and ask the user before shrinking fonts or wrapping to two rows.
### 4. Render to PNG
```bash
bash .agents/skills/dsh-arch-diagram/render.sh
```
Default output: `<repo>/assets/arch-en.png` and `<repo>/assets/arch-zh.png` — the paths that `README.md` and `README.zh.md` already reference.
Pass an alternate directory as `$1` to write elsewhere. `CHROME` and `PORT` env vars override the browser path and http.server port.
The script:
- Spins up a temporary `python3 -m http.server` on `127.0.0.1` (needed so Google Fonts CDN loads reliably in Chrome headless)
- Runs Chrome headless twice at `--force-device-scale-factor=2 --window-size=1536,580`
- Writes two 3072×1160 PNGs (~600 KB each)
- Tears down the server
### 5. Verify by eye — mandatory, do not skip
After rendering, **open each PNG and visually check every item** in this list. Do not report the diagram as done without going through it.
- [ ] Top-row card count matches the Default Services list you extracted in §1, every name rendered in full (no truncation on the longest, e.g. `ctx.systemPrompt` / `ctx.agentLoop`)
- [ ] Bottom-row card count matches the Capability Services list you extracted in §1, every name in full (especially the longest, e.g. `ctx.sessionPersistence`)
- [ ] Every card has a description underneath, and the description matches what the code does (not made up)
- [ ] Bar reads `Cordis · microkernel (vendored)` (EN) / `Cordis · 微内核 (vendored)` (ZH)
- [ ] Number of vertical connector lines equals (top-row cards + bottom-row cards) from §1. All straight vertical, all parallel, each reaching the horizontal center of its card
- [ ] `cordis.yml` sidebar visible on the right; dashed arrow points left into the bar
- [ ] `deployment leaf` / `部署清单` and `picks which plugins load` / `决定哪些插件加载` are readable and NOT crossed through by any connector line
- [ ] Chinese descriptions render as CJK glyphs, not `□` tofu boxes (means fonts loaded)
- [ ] No large empty gradient area at the bottom of the image — if there is, tune `--window-size` in `render.sh`
If any check fails: fix the CSS or template, re-run `render.sh`, re-check. Do not report "done" on first render without visually confirming.
---
## Design system (locked — do not modify without explicit user request)
Colors:
- Background: `linear-gradient(135deg, #ffffff 0%, #e8effc 100%)`
- Brand blue (Cordis bar + connectors): `#4a6ef5`
- Ink text: `#1a1a1a`
- Muted text: `#8a8f9c`
- Card border: `#dde3ef`
- Card fill: `#ffffff`
Typography (**sans-serif only** — never serif, never italic):
- English title & body: **Inter** (Google Fonts)
- Chinese title & body: **Noto Sans SC** (Google Fonts) with `PingFang SC` local fallback
- Code labels (`ctx.*`, `cordis.yml`, `vendored`): **JetBrains Mono** (Google Fonts)
Layout:
- `.page` max-width 1560 px, centered
- `.bus-row`, `.row.top`, `.row.bottom` all 96% width, centered
- Top row: `flex: 1` on each card (equal widths)
- Bottom row: `justify-content: space-between` + `flex: 0 0 auto` on each card — cards keep their intrinsic (nowrap) width and adjacent card backgrounds cannot clip a description
- Bus: `#4a6ef5` filled, 60 px tall, 8 px radius
- `cordis.yml` sidebar `position: absolute` at `left: calc(100% + 20px)` of `.bus-row`, width 130 px
- All connectors: CSS `::before` / `::after` pseudo-elements, 2 px wide, 40 px tall, `#4a6ef5`, centered via `left: 50%; transform: translateX(-50%)` — this is what guarantees pixel-perfect vertical parallel lines regardless of card content
Rendering:
- Chrome headless `--force-device-scale-factor=2`
- `--window-size=1820,580` → output is 3640×1160 (aspect ~3.14:1, wider than square-ish so a 12-card capability row fits without truncation)
- Local http.server on `127.0.0.1` so CDN fonts load cleanly
---
## Files in this skill
- `SKILL.md` — this file
- `harness-arch.css` — shared design system (colors, fonts, layout, connectors)
- `harness-arch-en.html` — English template
- `harness-arch-zh.html` — Chinese template
- `render.sh` — Chrome headless renderer
Templates are self-contained. Fonts load from Google Fonts CDN; no local font files.
---
## Rules I keep breaking (and shouldn't)
1. **Verify the output by looking at it.** Don't report "done" and move on — actually inspect each PNG and check every item in §5.
2. **Ground service descriptions in the code, not the docs' abstract, not the marketing copy.** If unsure what a package does, read its `src/` or the `ctx.<name>.register()` block.
3. **Don't touch layout unless asked.** 96% row/bar width, 40 px connector height, and 60 px bar height are all tuned. Card counts grow one at a time; once the bottom row starts to visibly overflow again (adjacent card backgrounds clipping a description, or the `cordis.yml` sidebar falling outside the render viewport), stop and ask before shrinking fonts, widening the viewport further, or wrapping to two rows.
4. **Never introduce a serif font or italic.** Not for titles, not for annotations, not "to make it feel editorial." This diagram is DeepSeek brand: sans-serif, upright, only.

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1536, initial-scale=1.0">
<title>DeepSeek Harness · Architecture</title>
<link rel="stylesheet" href="harness-arch.css">
</head>
<body>
<div class="page">
<p class="eyebrow">ARCHITECTURE</p>
<h1>System overview</h1>
<div class="diagram">
<p class="section-label top">DEFAULT SERVICES</p>
<div class="row top">
<div class="card"><span class="name">ctx.sessions</span><span class="desc">event-sourced log</span></div>
<div class="card"><span class="name">ctx.systemPrompt</span><span class="desc">prompt assembly</span></div>
<div class="card"><span class="name">ctx.tools</span><span class="desc">tool registry &amp; pipeline</span></div>
<div class="card"><span class="name">ctx.agents</span><span class="desc">agent handles &amp; events</span></div>
<div class="card"><span class="name">ctx.agentLoop</span><span class="desc">ReAct control loop</span></div>
</div>
<div class="bus-row">
<div class="bus">
<span class="core">Cordis</span>
<span class="dot">·</span>
<span class="core">microkernel</span>
<span class="vendored">(vendored)</span>
</div>
<div class="yml-side">
<div class="yml-arrow">
<svg viewBox="0 0 30 12" width="30" height="12" xmlns="http://www.w3.org/2000/svg">
<line x1="30" y1="6" x2="8" y2="6" stroke="#8a8f9c" stroke-width="1.2" stroke-dasharray="3 3"/>
<polyline points="8,3 2,6 8,9" fill="none" stroke="#8a8f9c" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>
</div>
<div class="yml-box">
<div class="file">cordis.yml</div>
<div class="sub">deployment leaf</div>
</div>
<div class="yml-note">picks which plugins load</div>
</div>
</div>
<div class="row bottom">
<div class="card"><span class="name">ctx.llm</span><span class="desc">LLM provider adapter</span></div>
<div class="card"><span class="name">ctx.bash</span><span class="desc">shell execution</span></div>
<div class="card"><span class="name">ctx.sandbox</span><span class="desc">process confinement</span></div>
<div class="card"><span class="name">ctx.codeRuntime</span><span class="desc">JS worker runtime</span></div>
<div class="card"><span class="name">ctx.fs</span><span class="desc">filesystem + policy</span></div>
<div class="card"><span class="name">ctx.skills</span><span class="desc">reusable skills</span></div>
<div class="card"><span class="name">ctx.web</span><span class="desc">search &amp; fetch</span></div>
<div class="card"><span class="name">ctx.compact</span><span class="desc">context compression</span></div>
<div class="card"><span class="name">ctx.subagents</span><span class="desc">spawn / fork / ACP</span></div>
<div class="card"><span class="name">ctx.workflows</span><span class="desc">dynamic orchestration</span></div>
<div class="card"><span class="name">ctx.sessionPersistence</span><span class="desc">JSONL / SQLite</span></div>
<div class="card"><span class="name">ctx.sessionQuery</span><span class="desc">unified log reads</span></div>
</div>
<p class="section-label bottom">CAPABILITY SERVICES</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1536, initial-scale=1.0">
<title>DeepSeek Harness · 架构</title>
<link rel="stylesheet" href="harness-arch.css">
</head>
<body>
<div class="page">
<p class="eyebrow cn">架 构</p>
<h1 class="cn">系统一览</h1>
<div class="diagram">
<p class="section-label top cn">默 认 服 务</p>
<div class="row top">
<div class="card"><span class="name">ctx.sessions</span><span class="desc cn">事件溯源日志</span></div>
<div class="card"><span class="name">ctx.systemPrompt</span><span class="desc cn">系统提示词组装</span></div>
<div class="card"><span class="name">ctx.tools</span><span class="desc cn">工具注册与执行</span></div>
<div class="card"><span class="name">ctx.agents</span><span class="desc cn">Agent 实例与事件</span></div>
<div class="card"><span class="name">ctx.agentLoop</span><span class="desc cn">ReAct 控制循环</span></div>
</div>
<div class="bus-row">
<div class="bus cn">
<span class="core">Cordis</span>
<span class="dot">·</span>
<span class="core">微内核</span>
<span class="vendored">(vendored)</span>
</div>
<div class="yml-side">
<div class="yml-arrow">
<svg viewBox="0 0 30 12" width="30" height="12" xmlns="http://www.w3.org/2000/svg">
<line x1="30" y1="6" x2="8" y2="6" stroke="#8a8f9c" stroke-width="1.2" stroke-dasharray="3 3"/>
<polyline points="8,3 2,6 8,9" fill="none" stroke="#8a8f9c" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</svg>
</div>
<div class="yml-box">
<div class="file">cordis.yml</div>
<div class="sub cn">部署清单</div>
</div>
<div class="yml-note cn">决定哪些插件加载</div>
</div>
</div>
<div class="row bottom">
<div class="card"><span class="name">ctx.llm</span><span class="desc cn">模型适配层</span></div>
<div class="card"><span class="name">ctx.bash</span><span class="desc cn">命令行执行</span></div>
<div class="card"><span class="name">ctx.sandbox</span><span class="desc cn">进程隔离</span></div>
<div class="card"><span class="name">ctx.codeRuntime</span><span class="desc cn">JS 运行时</span></div>
<div class="card"><span class="name">ctx.fs</span><span class="desc cn">文件读写</span></div>
<div class="card"><span class="name">ctx.skills</span><span class="desc cn">可复用技能</span></div>
<div class="card"><span class="name">ctx.web</span><span class="desc cn">网页搜索抓取</span></div>
<div class="card"><span class="name">ctx.compact</span><span class="desc cn">上下文压缩</span></div>
<div class="card"><span class="name">ctx.subagents</span><span class="desc cn">子智能体</span></div>
<div class="card"><span class="name">ctx.workflows</span><span class="desc cn">动态编排</span></div>
<div class="card"><span class="name">ctx.sessionPersistence</span><span class="desc cn">会话持久化</span></div>
<div class="card"><span class="name">ctx.sessionQuery</span><span class="desc cn">会话日志查询</span></div>
</div>
<p class="section-label bottom cn">能 力 服 务</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,220 @@
/* DeepSeek brand — bone-white + brand-blue, all sans-serif, no italic, no serif. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--bg-top: #ffffff;
--bg-bot: #e8effc;
--ink: #1a1a1a;
--muted: #8a8f9c;
--card-bd: #dde3ef;
--card-bg: #ffffff;
--brand: #4a6ef5;
--brand-soft: #dbe4fe;
--sans: 'Inter', 'Noto Sans SC', 'PingFang SC', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
--cn: 'Noto Sans SC', 'PingFang SC', 'Inter', -apple-system, sans-serif;
--mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--sans);
color: var(--ink);
background: linear-gradient(135deg, var(--bg-top) 0%, var(--bg-bot) 100%);
padding: 40px 64px 32px;
-webkit-font-smoothing: antialiased;
font-feature-settings: "kern" 1, "liga" 1, "cv02" 1, "cv11" 1;
}
.page {
max-width: 1560px;
margin: 0 auto;
}
/* ── header ─────────────────────────────────────────── */
.eyebrow {
font-family: var(--sans);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.22em;
color: var(--muted);
text-transform: uppercase;
margin-bottom: 10px;
}
.eyebrow.cn { font-family: var(--cn); letter-spacing: 0.4em; text-transform: none; }
h1 {
font-family: var(--sans);
font-size: 40px;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--ink);
margin-bottom: 88px;
}
h1.cn { font-family: var(--cn); letter-spacing: 0.02em; font-weight: 600; }
/* ── diagram frame ──────────────────────────────────── */
.diagram { position: relative; }
.section-label {
font-family: var(--sans);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.22em;
color: var(--muted);
text-transform: uppercase;
text-align: center;
margin-bottom: 20px;
}
.section-label.cn { font-family: var(--cn); letter-spacing: 0.4em; text-transform: none; font-size: 12px; }
.section-label.bottom { margin: 20px 0 0; }
/* ── card rows ──────────────────────────────────────── */
.row {
display: flex;
gap: 12px;
justify-content: center;
width: 96%;
margin: 0 auto;
position: relative;
}
.row.top .card { flex: 1; }
.row.bottom {
gap: 0;
justify-content: space-between;
}
/* flex: 0 0 auto so cards keep their intrinsic (nowrap) width and
neighbouring card backgrounds cannot clip a description. */
.row.bottom .card { flex: 0 0 auto; }
.card {
background: var(--card-bg);
border: 1px solid var(--card-bd);
border-radius: 6px;
padding: 14px 10px;
text-align: center;
min-width: 0;
position: relative;
}
.card .name {
font-family: var(--mono);
font-size: 13px;
font-weight: 500;
color: var(--ink);
line-height: 1.2;
white-space: nowrap;
display: block;
}
.card .desc {
font-family: var(--sans);
font-size: 11px;
font-weight: 400;
color: var(--muted);
margin-top: 4px;
line-height: 1.3;
min-height: 28px;
white-space: nowrap;
}
.card .desc.cn { font-family: var(--cn); }
.row.bottom .card { padding: 10px 6px; }
.row.bottom .card .name { font-size: 10px; }
.row.bottom .card .desc { font-size: 9px; min-height: 22px; }
.row.bottom .card .desc.cn { font-size: 10px; }
/* connectors — pure CSS, guaranteed vertical & parallel */
.row.top .card::after,
.row.bottom .card::before {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 2px;
background: var(--brand);
}
.row.top .card::after { top: 100%; height: 40px; }
.row.bottom .card::before { bottom: 100%; height: 40px; }
/* ── bus row (Cordis) ───────────────────────────────── */
.bus-row {
position: relative;
width: 96%;
margin: 40px auto;
}
.bus {
height: 60px;
background: var(--brand);
color: #ffffff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--sans);
font-size: 18px;
font-weight: 500;
letter-spacing: 0.01em;
}
.bus .dot { padding: 0 12px; opacity: 0.75; }
.bus .vendored {
font-family: var(--mono);
font-size: 15px;
font-weight: 400;
opacity: 0.8;
margin-left: 8px;
}
.bus.cn .core { font-family: var(--cn); }
/* ── cordis.yml side box (positioned outside bus width) ─ */
.yml-side {
position: absolute;
left: calc(100% + 20px);
top: 50%;
transform: translateY(-50%);
width: 130px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.yml-arrow {
position: absolute;
left: -30px;
top: 50%;
transform: translateY(-50%);
width: 30px;
height: 12px;
}
.yml-arrow svg { display: block; }
.yml-box {
width: 100%;
background: var(--card-bg);
border: 1px solid var(--card-bd);
border-radius: 6px;
padding: 12px 10px;
text-align: center;
}
.yml-box .file {
font-family: var(--mono);
font-size: 13px;
font-weight: 500;
color: var(--ink);
}
.yml-box .sub {
font-family: var(--sans);
font-size: 10px;
color: var(--muted);
margin-top: 4px;
line-height: 1.3;
}
.yml-box .sub.cn { font-family: var(--cn); letter-spacing: 0.05em; }
.yml-note {
font-family: var(--sans);
font-size: 10px;
color: var(--muted);
margin-top: 10px;
width: 100%;
text-align: center;
line-height: 1.4;
}
.yml-note.cn { font-family: var(--cn); letter-spacing: 0.05em; }

View File

@@ -0,0 +1,97 @@
#!/bin/bash
# render.sh — Chrome headless renderer for the DeepSeek Harness architecture diagram.
#
# Writes arch-en.png and arch-zh.png (3072×1160, ~600 KB each) to the given output
# directory. Default is <repo>/assets/, which is where the README image references
# resolve.
#
# Usage:
# bash render.sh # writes to <repo>/assets/
# bash render.sh /some/other/dir # writes there instead
#
# Env overrides:
# CHROME=/path/to/Chrome # non-standard Chrome location
# PORT=<n> # override the local http.server port
set -e
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SKILL_DIR/../../.." && pwd)"
OUT_DIR="${1:-$REPO_ROOT/assets}"
BIND_PORT="${PORT:-0}" # 0 → kernel picks a free port
SERVER_LOG="$(mktemp -t dsh-arch-diagram-server.XXXXXX.log)"
# Locate a Chrome / Chromium binary. Honour $CHROME if set; otherwise probe
# common per-platform paths so `bash render.sh` works on macOS + Linux out
# of the box.
if [ -z "$CHROME" ]; then
for candidate in \
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
"$(command -v google-chrome 2>/dev/null || true)" \
"$(command -v google-chrome-stable 2>/dev/null || true)" \
"$(command -v chromium 2>/dev/null || true)" \
"$(command -v chromium-browser 2>/dev/null || true)"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
CHROME="$candidate"; break
fi
done
fi
if [ -z "$CHROME" ] || [ ! -x "$CHROME" ]; then
echo "No Chrome / Chromium binary found." >&2
echo "Tried: macOS default, google-chrome, google-chrome-stable, chromium, chromium-browser." >&2
echo "Set CHROME=/path/to/binary and rerun." >&2
exit 1
fi
mkdir -p "$OUT_DIR"
python3 -u -m http.server "$BIND_PORT" --bind 127.0.0.1 --directory "$SKILL_DIR" \
>"$SERVER_LOG" 2>&1 &
SERVER_PID=$!
cleanup() { kill "$SERVER_PID" 2>/dev/null; rm -f "$SERVER_LOG"; }
trap cleanup EXIT INT TERM
# Discover the actual bound port from Python's own startup line
# ("Serving HTTP on 127.0.0.1 port <N> ..."). Works whether the kernel
# picked it or PORT was set explicitly; no external `lsof` needed.
PORT=""
for _ in $(seq 1 40); do
PORT=$(awk '/Serving HTTP on/ {print $6; exit}' "$SERVER_LOG" 2>/dev/null || true)
[ -n "$PORT" ] && break
# Bail early if the server died (e.g. explicit PORT already in use).
kill -0 "$SERVER_PID" 2>/dev/null || break
sleep 0.05
done
if [ -z "$PORT" ]; then
echo "Failed to detect local server port. Server log:" >&2
cat "$SERVER_LOG" >&2 || true
exit 1
fi
# Chrome/Chromium refuses to run as root without --no-sandbox. Rendering
# a local static HTML doc under a headless browser doesn't need Chrome's
# own sandbox, so passing the flag when EUID is 0 makes the script work
# in Codex/CI container environments that default to root.
CHROME_EXTRA_ARGS=()
if [ "$(id -u)" = "0" ]; then
CHROME_EXTRA_ARGS+=(--no-sandbox)
fi
for lang in en zh; do
out="$OUT_DIR/arch-$lang.png"
"$CHROME" \
"${CHROME_EXTRA_ARGS[@]}" \
--headless=new \
--disable-gpu \
--hide-scrollbars \
--force-device-scale-factor=2 \
--window-size=1820,580 \
--virtual-time-budget=8000 \
--screenshot="$out" \
"http://127.0.0.1:$PORT/harness-arch-$lang.html" \
2>/dev/null
size=$(du -h "$out" | awk '{print $1}')
echo "[$lang] wrote $out ($size)"
done