Commit Graph
106 Commits
Author SHA1 Message Date
lefarcenandGitHub c5ae6292c4 fix(daemon): state the skill read-only boundary instead of letting agents guess (#7720)
* fix(daemon): state the skill read-only boundary instead of letting agents guess

A user asked the agent to update one of his own skills. Every write to the
skill directory came back `Operation not permitted` — by design:
`resolveChatExtraAllowedDirs` keeps skill roots out of the writable
allowlist, and Codex is given no skill directory at all because it treats
`--add-dir` as write access (#622).

Nothing in the composed system prompt said so. With no stated boundary and
no stated exit, the agent invented one: it told the user to open Open
Design's settings and add the directory to a "文件系统 / 工作区 / 可写目录 /
writable roots" option. No such setting exists — not for writable roots, not
for a sandbox mode, not for an approval policy. The user went looking for
it, could not find it, and filed a diagnostics bundle (0.21.0, packaged
stable).

Adds an `## Editing skills` section that states the boundary and its
symptom, names the real edit surface (the Integration view's Skills tab —
NOT a "Settings > Skills" page, which does not exist), and forbids inventing
a settings path to explain the failure. The missing settings are worded as a
statement about the current build rather than a refusal, so contributors do
not read it as "we will never build this"; a controlled skill-write path is
still on the table.

Gated on `executionProfile === 'filesystem'`. Deliberately NOT gated on
`isSlimCore` — slim is the production default, so that gate would ship this
to nobody — nor on an active skill, since "update my skill" arrives on turns
where no skill is bound. Ask mode and API/BYOK mode have no file tools and
so cannot hit the boundary; the matrix snapshot pins that (18 of 20
scenarios receive it, `ask-mode-full-context` and `api-mode-byok` do not).

Red spec: tests/skill-write-guidance.test.ts, 5/5 red on this branch with
the `parts.push` removed, and 1/5 red (the wording guard alone) when
"does not currently expose" is rewritten to an absolute "we do not support".
Assertions are scoped to the extracted section rather than the whole prompt:
an earlier draft asserted /today/i document-wide and passed with no
implementation at all.

Note: like any prompt change this shifts the stable-prompt hash once, so
existing conversations take a single `stable-prompt-changed` cache miss on
first turn after upgrade.

Tested: pnpm --filter @open-design/daemon exec vitest run tests/skill-write-guidance.test.ts tests/system-prompt-template.test.ts tests/prompt-templates.test.ts tests/intent-signal-stable-prompt-cache.test.ts tests/official-system-prompt.test.ts tests/prompt-telemetry.test.ts tests/prompts tests/chat-route.test.ts (20 files, 355 tests)
Tested: pnpm --filter @open-design/daemon build
Tested: pnpm guard
Tested: pnpm typecheck
Not-tested: live packaged client — prompt-only change, no runtime surface

* fix(daemon): name the staged .od-skills copy as a non-persisting write

The boundary section told agents that writing a skill returns `Operation
not permitted`. That is true for the real skill root, and false for the
one skill path the prompt stack actually advertises:
`withSkillRootPreamble` names `<cwd>/.od-skills/<folder>/` as the primary
**Skill root**, and it sits inside the agent's writable project cwd. A
`Write`/`Edit`/`Bash` against `.od-skills/<id>/SKILL.md` therefore
succeeds, reads as an install, and is discarded when `stageActiveSkill`
re-stages the copy on the next turn — the same misdirection the section
was written to stop, minus the error that would have stopped it.

The handover line made that landing spot likely rather than incidental:
"write the proposed `SKILL.md` into the project folder" pointed at a
folder that already contains a directory labelled the skill root.

So: name the staged copy and say a write there does not persist, and
send the proposal to a new `.md` file in the project folder, explicitly
not under `.od-skills/`. The two new assertions are section-scoped and
go red against the previous wording — `/operation not permitted/` could
not, because on this path nothing is denied.

* fix(od-next): state the same skill read-only boundary on the strategy path

`server.ts` picks one composer per run:

    const prompt = odNextStrategyRecipe
      ? composeOdNextStrategyCorePromptV2(odNextStrategyRecipe)
      : composeSystemPrompt(systemPromptInputs);

so everything the previous commit added lives on the classic branch only.
When the OD Next strategy is assigned, `composeSystemPrompt` is never
called and there is no always-resident block underneath — the boundary
section disappears wholesale, and the reported failure comes back.

OD Next is not a path where this is hypothetical. It is the path with the
sharper version of the same trap: `materializeFrozenSkillPackage` writes
the frozen Skill package into `<cwd>/.od-skills/`, and the strategy prompt
hands the agent those exact roots — `materializedRoot` in the Skill roster
(`renderFrozenSkillRosterContext`) and `Frozen side-file root:` in each
Skill body (`resolveFrozenSkillBundleBodies`). Writable directory,
advertised path, no stated boundary, and a write there additionally breaks
the frozen identity the daemon verifies.

So the same four statements land in the core strategy prompt's
`## Agent and runtime boundaries`: the boundary, the materialized roots
that accept a write without persisting it, the real Skills-tab exit, and
the prohibition on inventing a sandbox / writable-roots / approval-policy
setting.

`assets.core.version` moves 2.1.0 -> 2.2.0 with the text. Package identity
is content-hashed from these bytes at apply time
(`buildStrategyPackageIdentity`), and `plugins-strategy-recipe.test.ts`
copies the real bundled folder, so that suite exercises identity against
the edited asset rather than a fixture.

The new assertions are scoped to the section (`.od-skills` also appears in
the task-profile assets, so a document-wide match could not go red) and
whitespace-normalized, since the asset is hard-wrapped prose. Five of the
six go red against the previous content; the sixth is labelled in-file as a
locator check that stays green on `main` by design.
2026-09-06 11:02:40 +00:00
Marc ChanandGitHub 1d2150007b chore: remove extracted landing-page from the monorepo (#7727)
* chore: remove extracted landing-page from the monorepo

The marketing site now lives in a separate repository. Drop the
Astro app, its GitHub workflows/scripts, and the remaining
monorepo coupling so this workspace no longer builds or deploys it.

* fix: drop leftover landing-page coupling from Teams and skills

Point development enterprise URLs at the deployed marketing origin,
remove nextjs-app skill outputs that no longer emit a page, and stop
advertising apps/landing-page in plugin marketplace metadata.

Generated-By: looper 0.12.0 (runner=fixer, agent=omp)

* fix(ci): restore DSH installer E2E Vitest routing

Removing dsh-bootstrap-identity dropped the only planner route from
tools/release/resources/dsh-bootstrap/ to web_tests_required, so
installer-only PRs skipped e2e/tests/dsh-installer-version-policy.test.ts.

Generated-By: looper 0.12.0 (runner=fixer, agent=omp)

* docs: drop leftover landing-page paths from template assets

The skill contract no longer advertises the extracted Astro app, but
styles.css, compose.ts, and example.html still named deleted
apps/landing-page sources. Describe the standalone template behavior
instead and keep the bundled plugin example in sync.

Generated-By: looper 0.12.0 (runner=fixer, agent=omp)

* docs: keep landing-page off review and test surfaces

Add apps/landing-page to the forbidden-surface review list and stop naming the extracted path in plugin registry acceptance tables.

Generated-By: looper 0.12.0 (runner=fixer, agent=omp)

* fix: drop leftover landing-page refs and bump plugin version

Stop naming the deleted apps/landing-page in the landing-deck demo and
plugin-registry plan. Bump example-open-design-landing to 0.1.1 and keep
the official catalog version aligned with the bundled manifest.

Generated-By: looper 0.12.0 (runner=fixer, agent=omp)
2026-09-03 03:21:08 +00:00
lefarcenandGitHub 09bd500d43 fix(plugins): enforce HTML preview contracts (#7707)
* fix(plugins): enforce HTML preview contracts

* fix: fail closed on preview contract I/O errors
2026-09-01 14:17:59 +00:00
6f081ef741 docs: map the two prompt implementations behind the OD Next fork (#7649)
* docs: map the two prompt implementations behind the OD Next fork

`composeSystemPrompt` returns early for OD Next runs, so the legacy stack below that return — the designer charter, the discovery layer, and the deck framework directive that embeds deck protocol v1 — never reaches those runs. Nothing in the repository said so, and prompt changes have been landing on one side only. The deck case is live: the OD Next `ppt.md` pins none of `data-od-deck-protocol`, the `od:deck-ready` announce, the `od:slide-state` posts, the `od:slide` listener, `id="deck-stage"`, or the print stylesheet, so a deck generated on that path can fail to be recognized as a v1 deck, paged from the host toolbar, or exported to PDF. Eligibility is re-evaluated per run and can latch down mid-session, which is why the divergence shows up as an intermittent bug.

Adds `apps/daemon/src/prompts/AGENTS.md` (fork point, the four variant axes, and a host runtime contract table naming which side carries each contract today) and `plugins/_official/scenarios/od-next-strategy/AGENTS.md` (asset roster and package-hash rules, and why maintenance notes must stay out of `assets/`, which is sent to the model verbatim). Root AGENTS.md gains an index entry and a Prompt variants section. Pointers go at the two fork sites, both deck-framework copies, and the OD Next atom registry, which is itself declared twice.

Documentation and comments only; no prompt bytes change. Validated with pnpm typecheck, pnpm guard, the daemon prompt suites (207 tests), and the contracts system-prompt / web-clone / deck-protocol suites (28 tests).

* docs: add PR #7568 as the worked example of a one-sided prompt change

The variant map says the two sides exist; it did not show what missing one costs a careful author. #7568 (merged 2026-08-29, `973e868ce`) is the case: it introduced deck protocol v1 across 18 files, handled the daemon/contracts prompt-copy split completely — shared constant, both copies changed, a guard added on each side — ran the full web and contracts suites plus two packaged DMG end-to-end runs, and touched nothing under `plugins/_official/scenarios/`. Its own stated goal was to stop new navigation dialects appearing; the OD Next path kept generating decks with no protocol at all, reported two days after merge.

Records the three reasons the miss stayed invisible, each still true today: both guards it added call `composeSystemPrompt` without an OD Next recipe so they cannot pass the fork (one is named 'ships new Agent decks with OD Deck Protocol v1' and is green), the prompt matrix snapshot updated cleanly because every scenario in it takes the legacy branch, and sharing a constant fixed only the machine-readable half — the surrounding directive prose had already drifted between the two copies before #7568 and still differs on main.

Documentation only. Validated with pnpm guard in a worktree on this branch.

* docs: mark the deck fork at the sites an agent actually edits

The file-top and fork-point pointers only reach an agent that reads from line 1. The edit an agent actually makes for a deck rule lands in the middle of DECK_FRAMEWORK_DIRECTIVE (368 lines below the daemon file's header) or at the push site (446 lines below the fork), typically reached by grepping and reading a short span — where no pointer was in view. Put a two-line marker at each of those four sites instead: above DECK_FRAMEWORK_DIRECTIVE in both prompt copies, and above the deck/freeform push guard in both, which covers both push branches. Line references in the two AGENTS.md files are re-pinned for the shift.

Comments only, placed outside every template literal. Verified byte-identical prompt output against origin/main: 8 scenarios x both composition paths, sha256 and length compared in one worktree with the source files swapped and contracts rebuilt — all 16 pairs identical. pnpm typecheck, pnpm guard, daemon tests/prompts (207), contracts system-prompt / web-clone / deck-protocol (28).

* docs: move the prompt variant map to docs/ and refresh it for #7651

Two changes in one, because main invalidated the old shape while it sat in review. Structural: the guidance moves from two nested AGENTS.md files to one `docs/prompt-composition.md`, matching how the root index already carries `docs/architecture.md` and `docs/skills-protocol.md`. Nested AGENTS.md is not auto-loaded on file access, so proximity bought no delivery that the root pointer and the in-file markers do not already provide. The plugin folder keeps a short AGENTS.md for the one rule that is only meaningful there — never write maintenance notes into `assets/`, which reaches the model verbatim. Naming it after the structure rather than the experiment keeps it valid if OD Next is retired or graduates: two of the four axes are independent of it.

Factual: #7651 closed the deck gap this document reported, by collapsing the scaffold to one source in contracts and reaching OD Next through `resolveOdNextDeckFrameworkMode` rather than the markdown assets. The contract table's deck rows now read supported on both sides, the daemon deck-framework file is a re-export, and the two markers that claimed OD Next never receives the directive are removed rather than reworded. #7568 and #7651 are now presented as a pair — a one-sided change and the follow-up nearly twice its size that repaid it. Remaining gaps are recorded: five daemon/contracts prompt pairs still hand-maintained, and `core-slim.ts` absent on the contracts side.

Documentation and comments only. Prompt output verified byte-identical against the merge base: 8 scenarios x both composition paths, sha256 and length, markers reverted and reapplied in place. pnpm typecheck (0 errors), pnpm guard, daemon tests/prompts (210), contracts system-prompt / web-clone / deck-protocol / od-next-prompt-recipe (53).

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
2026-09-01 06:26:59 +00:00
cd6920f963 feat(prompts): require real imagery for factual referents (#7436)
* feat(prompts): require real imagery for factual referents

* fix(prompts): preserve content image aspect ratios

* fix(prompts): resolve slim charter imagery rule conflicts

- Move the factual-integrity carve-out into instruction-priority item 2 so
  it can actually win a conflict against a skill, instead of sitting after
  the list as charter prose at priority 4.
- Qualify the Conduct copyright rule so it composes with the requirement to
  acquire real brand marks, covers, and artwork, closing the documented
  failure where the agent silently substitutes a look-alike.
- Restore the charter byte budget at 29_696 (was deleted, previously 25_600)
  so future growth stays a deliberate, explained decision.

---------

Co-authored-by: Mason6660201 <13073456345@163.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-08-27 03:15:49 +00:00
f1a73f0d89 feat(od-next): structure-only layout primitives for the prototype task profile (#7327)
* feat(od-next): ship structure-only layout primitives with the prototype task profile

Blind evals and the pet-grooming runs showed the same layout defects over and
over — stacked spans that never became blocks, chrome height constants reused
as padding, authored copy crammed until it wrapped — while the prose rules that
forbid them sat unchanged in the prompt. The rules are now backed by something
the Build can copy instead of re-derive:

- `assets/task-profiles/prototype/layout.css`: a ~50-line, structure-only
  stylesheet (`@layer od-layout`, `:where()`, `od-` prefix) for stacked
  information, truncation/clamp of data text, media ratio, rails, and an
  optional screen skeleton. Declared as a prototype resource, staged into
  `.od-frames/` under the existing ownership manifest, and quoted into the
  stable request context as the `layout-primitives` fact on every prototype
  run.
- Prototype rule card 2.2.0: a "Variable-length text and stacked information"
  section that separates authored copy (rewritten to a budget, never
  truncated) from data text (truncate/clamp in lists, wrap in full on
  confirmation and detail screens), keyed to the real class names; the quality
  floor's layout bullet now points at the primitives.
- Core 2.1.0: the variable-length-text paragraph rewritten as actions
  (three fates before the text is written) instead of prohibitions.
- Run-finish observation `od_next_layout_primitives`
  (verbatim / modified / linked / absent) so the rollout can tell whether a
  model tier reproduces the block faithfully enough for inlining.

* fix(od-next): let aspect-ratio win on .od-media and pin two rule-card gaps

The first verification runs exposed one primitive bug and two recurring model
slips: `.od-media` lacked `height: auto`, so an `<img height=…>` attribute
kept its pixel height and the ratio never applied (tall cropped thumbnails);
cards built on `<a>` shipped with browser-default link styling; and product
tokens declared on an inner wrapper left sheet/dialog overlays mounted in the
shell's screen without a surface. The stylesheet and the rule card now say so.

* fix(od-next): keep .od-media at its intrinsic ratio; make cropping an explicit opt-in

Review: the fallback aspect-ratio of 1 plus an unconditional object-fit:
cover turned every unset .od-media into a square crop — contradicting the
core image-geometry rule (content-bearing images render their full frame;
cover is reserved for deliberate crops) and the primitive's own comment,
exactly on the media the rule card points at product shots. The primitive
now defaults to aspect-ratio: var(--od-ratio, auto) with no object-fit;
a uniform tile crop sets --od-ratio and adds .od-media-cover together,
and the commerce-card row of the rule card says so.

* fix(od-next): carry finish-time observations on run_finished, not the frozen baseProps

Review: od_next_device_platform*/od_next_layout_primitives were spread into
baseProps, which is built at run creation — before any deliverable exists —
and run_finished only spreads that frozen object. The daemon log printed the
observations while the analytics event never carried them, which defeats the
rollout signal these fields exist for. They now join finishedProperties,
read live from the run after the physical run resolves. (The device-shell
fields shipped with the same trap in the shells PR; fixed here on the same
lines.)

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-08-25 03:58:46 +00:00
open-design-crew[bot]GitHubCheemslefarcenCheems黄桃open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>NJUHuahihijack111
7320f9b8ab feat(daemon): make OD Next the default design strategy (#7016)
* refactor(observability): isolate legacy run exporter

* refactor(daemon): extract internal run creation service

Route /api/runs and compatibility /api/chat through one daemon-owned create, claim,
resume, and start seam. Preserve HTTP responses, SSE timing, session bookkeeping,
and durable Run formats.

Validated with daemon tests/build/typecheck, workspace guard, and workspace typecheck.

* test(plugin-runtime): freeze OD Next strategy fixture boundaries

Add exact pre-Build route and stage cases plus deterministic deletion-boundary mutations without shipping an evaluation harness.

Preserve the current official and community fallback pipeline and composed verification/critique prompt sections as golden witnesses.

Validated with plugin-runtime tests and types, focused daemon prompt composition, daemon types, guard, and workspace typecheck.

* fix(daemon): finalize terminal telemetry delivery

Persist a versioned delivery record so normal failures stop after bounded retries
and only an unfinished crash window resumes on restart.

Reuse the persisted identity across Vela and Relay transport, isolate stable
direct Langfuse event ids by delivery purpose, and log only redacted sink data.

Validated with focused daemon and contracts tests, workspace typechecks, and
guard. The full daemon suite had one timeout; its four-test file passed alone.

* feat(plugins): add OD Next strategy scenario and contracts

Ship the V2 strategy package as inactive bundled content and validate its contracts without changing ordinary plugin manifest compatibility.

Keep bundled strategy bytes out of the catalog and generic apply path until a later hash-gated activation owner binds them.

* feat(observability): add normalized agent observation contract

Define a pure versioned contract for identity, Prompt, usage, timing, and status evidence with explicit availability and per-value provenance.

Add a structured main-Run compatibility adapter that preserves unknown facts, separates clocks, and omits provenance-ambiguous derived totals without changing legacy export behavior.

* feat(plugins): bind OD Next strategy content identity

* feat(daemon): persist OD Next strategy task chains

Add the daemon-owned SQLite truth for immutable strategy task identity, ordered physical Run mappings, canonical Plan Contracts, revision-CAS transitions, and terminal reconciliation.

Pin task-owned Snapshots and exclude their durable references from pruning so mixed GC sweeps remain safe.

Validated with 103 focused daemon tests, contracts tests/typecheck, daemon and workspace typechecks, and guard.

* feat(daemon): gate OD Next runtime capabilities by fixtures

Add exact CLI/adapter/fixture tuple resolution, stable capability snapshots, and offline
replay graph invariants for the four initial runtime paths.

Keep production registry empty until sanitized real fixtures exist; synthetic evidence and
version drift remain unknown.

Validated with contracts tests, focused daemon and e2e boundary suites, guard, and
workspace typecheck.

* feat(daemon): add hash-gated OD Next prompt recipe

Compose the bundled strategy from validated content, one Task Skill, and the exact stage policy.
Fail closed on package, profile, pipeline, or atom drift while preserving ordinary quality paths.

Validated with focused tests, package typechecks, workspace typecheck, and guard.

* feat(observability): aggregate OD Next task observations

Build stable task, Run, Child, model, and tool hierarchies from durable StrategyTaskExecution mappings and normalized facts while keeping missing coverage explicit.

Map task aggregates to legacy ingestion behind the consent gate. Preserve owner/exclude hierarchy while accounting usage once, enforce kind-specific Prompt boundaries, and accept absolute time only from one Unix-epoch evidence record.

Validated with 228 focused daemon tests, 353 contracts tests, daemon/contracts and workspace typechecks, guard, and the task harness.

* feat(daemon): collect bounded Codex child evidence

Read declared Codex rollout sessions without scanning arbitrary homes.
Normalize verified child facts while redacting prompts and preserving Run semantics.

* feat(daemon): add OD Next planning coordinator

Add a daemon-owned planning reducer, deterministic Resolver and Preflight facts, and a stream-safe machine protocol boundary backed by the durable task store.

Lock one clarification and one serialization-only repair across restart, preserve the Plan hash, and keep ordinary Run text unchanged.

* feat(daemon): collect native OpenCode child evidence

* feat(observability): add Langfuse OTLP task exporter

Map normalized task observations to complete OTLP/JSON v4 spans with stable parent context, allowlisted metadata, usage accounting, and bounded terminal delivery.

Fail closed on drift across root and operation input, model, timing, type, hierarchy, status, and complete usage provenance while dual remains a local-only OTLP shadow.

Keep legacy rollback, consent gates, and X5 live self-host verification boundaries intact.

* feat(daemon): normalize Claude native child evidence

Collect matched Claude Task sidechain lifecycle facts without changing main stream turn, stdin, or error bookkeeping, and adapt them to provider-neutral observations with unavailable Prompt and usage evidence.

Bind session, Task id, and parent identity immutably; poison rebinding, session drift, and conflicting terminals so ambiguous evidence cannot reach L2. Keep production capability unknown until X1 supplies an exact-version sanitized real fixture.

* feat(daemon): consume versioned Vela child evidence

Negotiate schema v1 only on AMR and enforce run-scoped start-to-terminal graph, status/source coherence, and exact usage source/completeness bindings. Emit allowlisted Normalized child facts without Vela private I/O.

Pin the unpublished producer golden for offline compatibility while keeping production capability unknown until the X1/X3 evidence gates are satisfied.

Validated with 147 focused daemon tests, 353 contract tests, daemon/workspace typechecks, guard, and the task harness.

* feat(daemon): automate OD Next simple production

Connect parsed OD Next terminal output to the durable coordinator, claim clarification, repair, and production Runs atomically, and start continuations only after the source Run ends.

Converge task failure and cancellation before physical end, require daemon-owned execution facts and canonical delivery, and keep ordinary Runs compatible.

Validated with focused daemon route and coordinator tests, contracts tests, daemon and workspace typechecks, guard, and the task harness.

* feat(daemon): enforce verified complex production

* feat(web): follow OD Next task chains across Web and CLI

Persist daemon-issued task handles and visible predecessor-prefix boundaries, then follow projected active Runs through one terminal outcome in Web and od run.

Normalize reloads to daemon activeRunId before subscribing, transfer controller ownership atomically across successors, discard stale cursors, and preserve visible prefixes without replaying predecessor machine payloads.

Validated with focused Web and daemon tests, contracts tests, workspace typecheck, guard, diff checks, and the task harness.

* feat(daemon): roll out OD Next task observability

* feat(daemon): add controlled OD Next rollout

* fix(daemon): align OpenCode and Vela evidence tuples

Pass GPT-5.6 Sol's verified high variant to OpenCode, pin native evidence to 1.18.18, retain locally recorded success and failure seeds as explicitly unattested, and align the Vela consumer golden to the local versioned producer.

Validated with 5 focused daemon files (175 tests), daemon and workspace typechecks, guard, the real local Vela ACP Sol smoke, and a minimal test-relay telemetry write.

* fix(daemon): honor OpenCode model variant capabilities

Parse the live verbose OpenCode catalog per model and forward only variants advertised by the exact detected binary. Keep fallback and one-off OPENCODE_BIN connection tests on the base model when variant support is unknown.

Record machine-checkable OpenCode 1.18.18 root, tool, child, recovery, cancel, timeout, and resume evidence without promoting the unattested seed into the production capability registry.

Validated with focused runtime and connection tests, host process-group and watchdog tests, contracts tests, daemon/workspace typechecks, guard, and an isolated SIGKILL delivery-recovery exercise.

* feat(daemon): add OD Next analytics operations report

Reduce normalized task aggregates into source-aware operations metrics without
copying the Langfuse dashboard or exposing user trace links.

Preserve main/Child accounting, stage-scoped coverage, unavailable facts, and
external-signal-only quality inputs.

Validated with focused and contract tests, the full daemon suite, guard,
workspace typecheck, and exact-trace Langfuse readback.

* feat(observability): complete child prompt and runtime provenance

Export bounded redacted child-injected Prompt content and exact CLI, companion, and
adapter versions through task-level legacy and OTLP mappings.

Register the OpenCode 1.18.18 best-effort replay tuple while keeping unknown versions
and incomplete Codex evidence fail closed.

Validated with 353 contracts tests, 196 focused daemon tests, daemon/workspace
typechecks, and guard.

* fix(daemon): recognize Codex child failure terminals

Parse Codex 0.147.0 Child task_complete.error as the runtime-owned failed terminal, honor session_meta id over its legacy root session_id, and map interrupt_agent aborts to canceled.

Add a redacted seven-path best-effort replay and exact Codex capability tuple. Unknown versions remain fail-closed and ordinary Codex runs are unchanged.

Validated with 92 focused daemon tests, 353 contracts tests, daemon and workspace typechecks, guard, and the 25-unit task harness.

* fix(daemon): report verified child runtime versions

Use versions observed from resolved CLI probes or Vela's ACP handshake when annotating Child observations. Correct Native OpenCode's allowlist key, add Claude and Vela/OpenCode companion provenance, reject Vela probe/handshake disagreement, and ignore the legacy 0.0.0 placeholder.

Validated with 252 focused daemon tests, daemon and workspace typechecks, guard, the task harness, and a real local Vela --version/-v/ACP initialize build smoke.

* test(observability): record Vela child evidence matrix

Add a sanitized provider-backed Terra replay for main, tool, child success, child failure with parent recovery, cancel, timeout, and cross-process resume. Keep the unpublished Vela tuple outside the production registry while pinning its exact candidate and OpenCode companion evidence.

Validated with focused Vela consumer/capability tests, daemon typecheck, workspace typecheck, guard, and the task harness.

* feat(daemon): enable OD Next production profiles

Make prototype, presentation, marketing, and HyperFrames the exact global OD Next production set while preserving explicit off/observe, user plugin authority, and fail-closed runtime capability gates. Feed runtime-owned profile, route, output, input, and capability facts into the planning contract and keep all other artifact kinds on ordinary Open Design.

Use bounded executable-scoped CLI version probes so headless and restarted daemons can resolve exact tuples without a prior agent-list request or cross-binary cache contamination.

Validated with four provider-backed Terra CLI chains, focused daemon/contracts/plugin-runtime suites, daemon and workspace typechecks, guard, and the 25-unit task harness.

* feat(daemon): classify OD Next exact agent inputs

Route prompt contributors through one daemon-owned choke point and version their final-text, semantic-fact, transport, and exclusion ownership for the forthcoming canonical bundle. Send OD Next continuation stages directly through native sessions without rewrapping or re-seeding stable instructions.

Validated with focused daemon and contracts tests, daemon/workspace typechecks, and guard.

* fix(daemon): close OD Next input ownership gaps

Bind semantic request facts to their real daemon-system or request producers, cover the DSH profile exact-text transport, and reject lookalike prompt-bundle root names.

Validated with focused exact-input, production-server and DSH tests, daemon typecheck, task harness, and guard.

* feat(daemon): emit canonical OD Next prompt bundles

Add strict pure TypeScript Bundle and request-Turn serializers, then route OD Next request and continuation text through their versioned XML contracts.

Split Web prior transcript from the current turn without text subtraction, keep mutable Skill and attachment inputs explicitly pending, and preserve ordinary prompt behavior.

Validated with focused contracts, daemon and web tests, the real server fixture, package and workspace typechecks, and guard.

* fix(daemon): align OD Next bundle input ownership

Register the real canonical Bundle contributors and keep current-turn, prior-transcript, pending-fact, and research-query ownership explicit at the production seam.

Reject continuation task_run_index zero and cover Web framing, ordinary compatibility, fail-closed contributor drift, and real server prompt capture.

* feat(daemon): freeze OD Next user skills

Capture explicit Skill bodies and audited side files before task claim, persist verified task-owned bytes, and replay only the frozen package for OD Next runs.

Keep legacy single-Skill CLI requests and ordinary live staging compatible while adding Web/CLI multi-Skill coverage and fail-closed snapshot checks. Validated with focused server, contract, CLI, Web, typecheck, and guard suites.

* fix(daemon): harden OD Next skill snapshots

Require an explicit frozen package for every mapped OD Next task and fail corrupt task state before any live Skill staging.

Use bounded no-follow descriptor reads with race checks, preserve legacy CLI compatibility, and make materialized roots collision-resistant. Validated with focused, real-server, workspace typecheck, and guard suites.

* feat(daemon): freeze OD Next task input snapshots

Freeze OD Next attachments and canonical configuration before the atomic run/task claim, then route runtime access through durable task-scoped snapshots while preserving ordinary attachment behavior.

Validate immutable identities, transport references, continuation propagation, tamper failure, and the four production task types with focused contract and daemon coverage.

* fix(daemon): isolate OD Next runtime input projections

Keep canonical task snapshots daemon-only, rebuild a read-only per-Run projection, and deliver frozen document resources through ACP or OD_TASK_INPUT_DIR.

Harden source and snapshot reads with no-follow same-fd identity checks and bounded caps. Validated with focused runtime tests, contracts, daemon/workspace typechecks, guard, and the task harness.

* fix(daemon): harden OD Next snapshot transport boundaries

Reject intermediate symlinks and realpath escapes while loading canonical snapshot files, including identical-byte replacement targets.

Preserve ordinary ACP duplicate image ordering while suppressing only OD Next resource/image cross-duplicates. Validated with focused tests, daemon typecheck, guard, and the task harness.

* fix(daemon): preserve structured OD Next snapshot failures

Defer frozen Skill package errors from the Task 04 preclaim lookup to the existing post-claim failure boundary so mapped Runs remain durable JSON failures without orphan input snapshots.

Validated with the combined real-server repair, missing-package, and tampered-package regressions.

* fix(daemon): keep OD Next continuation cwd stable

Separate authoritative project cwd resolution from best-effort file inventory and tolerate entries deleted between directory enumeration and stat.

This keeps native-session identity stable while frozen task inputs replace live attachments during continuation.

* feat(daemon): persist exact OD Next prompt inputs

Persist the canonical first-turn Bundle and every deterministic continuation Turn with UTF-8 and SHA-256 identities, then send only those validated texts through runtime transports.

Fail closed on mapped task drift, tamper, legacy NULL identities, missing mappings, and cold continuation while leaving ordinary Runs on the existing live composer.

Validated with the 15-case automatic server acceptance twice, 223 focused daemon tests, daemon typecheck, guard, and the task harness.

* feat(daemon): observe exact OD Next prompt inputs

* feat(daemon): bind and observe Claude native subagents

Register the replay-verified Claude 2.1.233 tuple, bind complex Build Packages to daemon-issued native Agent handles, and derive completion only from durable child lifecycle evidence.

Export bounded parent-tool, Child Agent, and Child-tool spans through the existing legacy and OTLP task hierarchy without exposing raw tool inputs, outputs, provider ids, paths, or credentials.

Validated with focused contracts and daemon suites, the 17-case real-server chain, workspace typecheck, guard, and the 26-unit task harness.

* fix(daemon): repair OD Next rollout authority and controls

* fix(daemon): harden OD Next public canaries

* refactor(daemon): extract OD Next prompt preparation

* test(daemon): align plugin fixture with project schema

* test(e2e): assert daemon-owned prototype routing

* fix(daemon): restore OD Next task observability

Default eligible OD Next tasks to privacy-aware Langfuse hierarchy delivery through relay or direct sinks, with a versioned safe quality projection.

Persist lightweight representation ownership before suppressing mapped Runs, retain best-effort startup retry semantics, and preserve single-Run compatibility when Task delivery is not eligible.

Adopt the active payload relay for historical packaged launchers and cover the real sidecar environment handoff.

Validated with the full daemon suite, focused contracts/packaged/e2e/tools-pack suites, workspace guard, and workspace typecheck.

* fix(daemon): make OD Next capability fallback invisible

Admit capability-certified runtime adapters across CLI version drift while keeping recorded fixture versions as auditable evidence and reserving a narrow incompatibility denylist.

Fall back before Run claim on preparation failures, keep continuation bound to the frozen capability snapshot, and report truthful child coverage across Langfuse legacy and OTLP without replaying an active task.

Validated with focused contracts and daemon suites, the automatic server suite, pnpm guard, pnpm typecheck, daemon typecheck, LSP diagnostics, and git diff --check.

* fix(strategy): isolate OD Next automatic task routing

* fix(daemon): persist blocked strategy-task attribution

Every blocked strategy task now stores its gate reason codes and the
agent-visible turn text (blocked_reason_codes_json / blocked_visible_text)
so operators can diagnose a blocked task from the store alone. Covers the
coordinator blockTask gate, the automatic-continuation fail-closed path,
and startup run-terminal reconciliation.

* fix(daemon): accept form-only clarification turns by deterministic inference

A first Full-Plan request turn that renders exactly one question form and no
machine-protocol block has exactly one valid protocol meaning: outcome
clarification_required with an unlocked execution mode. The coordinator now
recovers that state deterministically instead of dead-ending the task as
blocked, stamps od_next_protocol_runtime_state_inferred into the accepted
state's reasonCodes for attribution, and keeps every ambiguous shape (extra
forms, recovered plan blocks, later stages, spent clarification budget,
any other protocol issue) fail-closed. This closes the observed field
failure where the agent's first reply produced a discovery form, the task
went blocked, and the user's form answer then failed with 409
STRATEGY_TASK_STATE_MISMATCH.

* feat(contracts,daemon): project blocked strategy-task attribution to clients

A task the OD Next protocol gate settles as blocked persists its
attribution (reasonCodes + agent-visible text) since 8bc7805e62, but the
client-facing StrategyTaskProjectionV2 dropped it, so the UI could not
tell a blocked task from any other failure. Add the optional
blockedContext field to the projection contract (additive, strict shape
mirroring the task store) and have projectStrategyTask carry it through
every run-status / SSE assembly point, only on a blocked outcome.

* fix(web): terminate question-form interaction when a strategy task is blocked

A blocked OD Next task is a sticky terminal verdict, but the web client
kept the turn's discovery/clarification form fully submittable; the
answer then 409'd with STRATEGY_TASK_STATE_MISMATCH and surfaced only
the generic 'Task failed' card.

- ChatMessage gains additive strategyTaskBlocked / strategyTaskBlockedText
  fields; strategyBlockedMessageFields derives them from any terminal
  blocked projection (visibleText preferred, trimmed, else null).
- streamViaDaemon/reattachDaemonRun expose onStrategyTaskSettled with the
  terminal task projection; ProjectView stamps the blocked verdict onto
  the assistant message on live settle, on reattach settle, and on the
  reattach status probe, and persists it so it survives reloads.
- AssistantMessage disables the inline question form for a blocked turn
  (including the auto-continue countdown) and renders the gate's visible
  text - or a generic localized notice - under the form.
- resolveRunFailureUi maps STRATEGY_TASK_STATE_MISMATCH to dedicated
  halted-task copy instead of chat.runError.title.generic.
- New i18n keys (title, 409 message, form notice) added to types.ts and
  all 19 locales.

Blocked-state interaction shape is a minimal safe default pending a
product decision: disable submission and explain, no auto-retry, no
degraded re-run.

* fix(daemon): normalize premature clarification execution mode

The observed 2/2 field failure on vague first prompts: the agent emits a
well-formed Runtime State block for clarification_required but predicts
"executionMode": "simple" alongside it. The mode is daemon-locked only
after clarification resolves, so the prediction carries no authority — yet
strict schema validation classified the whole block as
od_next_protocol_runtime_state_invalid_schema (non-repairable) and
dead-ended the task. The protocol stream now discards exactly that field
before validation, records the normalization for attribution, and the
prompt recipe gains an explicit rule plus a clarification-shape Runtime
State example so agents stop copying the plan_ready example's mode.

* fix(strategy): declare the runnable-entry deliverable convention to agents

The deliverable validator requires a root-level runnable entry (root
index.html for html deliverables), but neither the frozen prompt bundle nor
the production continuation ever stated that convention — the agent was
told only 'editable prototype source with a stable runnable entry', built a
framework scaffold in good faith, and the completed task then blocked with
od_next_canonical_deliverable_invalid. The production continuation and the
prototype Task Skill now state the root-entry convention and forbid
unplanned framework/dependency scaffolds, so the contract the daemon
validates is the contract the agent was given.

* fix(daemon): stop the rollout only for real machine-contract failures

rolloutStopSignalForBlockedContinuation mapped every reason code containing
'protocol' to machine_contract_leak, whose stop mode is a hard instance-wide
off that survives restart. Two vague prompts were therefore enough to
silently disable OD Next for the entire daemon: the agent emitted a
clarification runtime state carrying a premature executionMode, the task
blocked, and every later request in the daemon fell back to the legacy
plugin with no UI signal and no automatic recovery.

The machine block never reaches the user — the protocol stream suppresses
undelimited and oversized bodies — so an agent turn that omits, duplicates,
or mis-populates a block is a per-task defect, already fail-closed for that
task and now attributable from the store. Only the two block-delimiting
failures keep raising the leak signal.

* fix(strategy): state the entry-resolution constraint without inventing a product rule

The previous wording told agents that a prototype's runnable entry must be a
root index.html and that framework scaffolds are forbidden. Neither is in the
product spec, which says only that the user must be able to open and use the
deliverable from a real entry point, and neither matches the validator, which
resolves an entry through root index.html, then a single root-level html file,
then a single kind-matching file. That turned one fallback branch into a
product constraint the agent then had to obey.

Both surfaces now state the resolution rule the daemon actually applies and
why a delivery is rejected without it, and take no position on whether a
prototype may be an application project. Whether the validator should also
resolve framework-project entries is a product decision and stays open.

* fix(strategy): assemble the OD Next prompt bundle as an XML tree

The first-turn payload was an XML envelope around four flat CDATA blobs,
with one of them holding 25.3KB of joined Markdown (89% of the bundle).
Three layers each threw structure away: the contract typed the bundle as
four strings, the daemon joined 15 individually-named contributors with
'\n\n---\n\n', and the recipe re-joined 10 sections while demoting the
stage/atom map to Markdown headings. The result inverted heading levels in
7 of 46 places and put a per-task UUID 1,268 bytes into the payload, which
pushed 23KB of byte-identical strategy content past the point where a
provider prompt cache can share a prefix.

Introduce a canonical XML tree primitive and a v2 bundle schema that nests
one element per spec slot. Because system_prompt is now the shared cache
prefix, it may hold only cross-task-constant content, so the recipe
identity, runtime planning facts, runtime tool environment, client system
prompt and form override all move into context, and user_prompt moves last.

Measured on the real strategy assets: 28,325 -> 26,010 bytes, 4 -> 16 CDATA
leaves, 7 -> 0 heading inversions, and the first byte that differs between
two tasks moves from offset 1,268 to 24,709 (past the end of system_prompt).

Persisted rows keep working: final_text_schema is dispatched per row, so a
v1 bundle still parses at its own version while new tasks mint v2.

* feat(daemon): let the main Agent choose the task route

Product spec 3.1 makes the main Agent the party that decides Direct Edit vs
Full Plan, and 6.1 puts that decision inside MVP scope. The daemon instead
locked the route before the Bundle was ever sent: routes/runs.ts passed
preference 'full_plan' with all five Direct Edit eligibility facts hardcoded
false, and decideStrategyRequestRoute short-circuits on that preference, so
Direct Edit was structurally unreachable. A local edit on an existing
artifact therefore paid a full planning round — measured at 2 Request Turns
and 61s for a colour change that the Agent itself correctly scoped to one
property.

Four of the five eligibility facts require reading the request, which the
daemon cannot do before dispatch. So the route now stays unlocked through
the request turn — a state the task store and the projection contract both
already model — and the daemon adopts the route the Agent declares in its
Runtime State. Every later turn keeps the existing lock: a route may never
change once set.

The Full-Plan-only guards for clarification inference, serialization repair,
and the repair anchor now also accept the unrouted first turn, since each of
those paths is itself a Full Plan declaration and locks the route as it
transitions. blockTask settles an unroutable turn on the spec's documented
fallback route rather than refusing to record it.

No contract field, DB column, or prompt byte changes: the recipe already
tells the Agent to choose the allowed route on the request stage, and
Runtime State has always carried .

* fix(strategy): tell the Agent it owns the request-stage route choice

Unlocking the route in the daemon was necessary but not sufficient. The
recipe still read "use the supplied task facts to choose the allowed route"
and immediately followed with "For a Full Plan route, the request and
clarification stages are planning-only", so the Agent kept assuming a Full
Plan had already been chosen for it. Verified through the real UI: an
unambiguous local edit still came back as full_plan with the Agent stating
'this turn will not modify files'.

The request-stage section now says the choice is the Agent's, that Open
Design leaves the route unset until the first Runtime State declares it,
and that the active orchestration Skill's eligibility conditions decide
between the two. It states the mechanism and defers the criteria to the
Skill rather than restating them.

Verified end-to-end in Chrome against a real codex run: a local colour edit
on an existing artifact now routes direct_edit / simple and completes in
ONE Request Turn (50s), matching spec 3.4. Ambiguous requests still route
full_plan, and a request needing clarification still routes full_plan
because Direct Edit is request-only.

* fix(observability): restore failed-Run process evidence in the task hierarchy

PR #7016 replaced the per-Run Langfuse trace with a task hierarchy
(`strategy-task:<taskExecutionId>` + `strategy-stage:*` child spans) and
durably suppresses the single-Run trace. Failure classification survived
the move, but the terminal process evidence the single-Run trace carried
— stderr/stdout tails, host close diagnostics, exit code and signal —
did not reach Langfuse at all, so a failed Run lost the only facts that
explain *why* it failed.

Carry those facts through the existing safe-projection boundary:

- `SafeRunQualityV1` gains an optional `process` section
  (`exitCode`, `signal`, `stderr`, `stdout`, `diagnostics`), with
  `SafeObservationStreamTailV1` and a bounded `SafeRunDiagnosticsV1`
  record that admits only booleans and identifier-shaped values.
- `buildSafeRunQualityProjectionV1` builds it. Stream tails reuse the
  producer's existing 20-line / 4 KiB cap plus `redactSecrets`, then add
  the wider Prompt-stack local-path masking before `safeQualityText`.
  stderr follows the run error-message rule; stdout is the agent's own
  output and needs content consent, reporting its line count with an
  explicit limitation when withheld.
- `buildSafeRunQualityProjectionFromDaemon` collects the same summaries
  the single-Run bridge collected, and the aggregation projects them
  onto the `strategy-stage:*` span metadata for both the legacy and OTLP
  exporters.

The smoke test now asserts the task-hierarchy shape (trace id, child
span id/name, failure metadata keyed by runId) and re-asserts the
diagnostics coverage there, including that a local path in stderr is
masked before transport. Each case gets its own daemon and `OD_DATA_DIR`
via `vi.resetModules()`: the OD Next rollout stop latch lives in SQLite
and fires per daemon instance, so a shared data root silently pushed
later cases onto the compatibility single-Run reporter. The one case
that legitimately stays on that reporter (deepseek, outside the OD Next
runtime path) now asserts the same coverage on the single-Run trace.

* test(daemon): make the run-failure smoke deterministic under load

The failed-run smoke could report `expected 'process_exit' to be 'auth'`
under CPU pressure. Two independent sources, both closed here.

1. The fake `claude` was not a valid OD Next Claude runtime. `detectAgents`
   parses `claude -p --help` for every flag in the runtime def's
   `capabilityFlags`; once a physical Run is mapped to an OD Next strategy
   task, `buildArgs` sets `observeNativeChildBehavior` and THROWS when the
   probe never saw `--forward-subagent-text`. The fake advertised neither
   that flag nor `--agents`, so whenever the task mapping was already
   visible to `startChatRun` the Run died on that host-side TypeError
   before the agent was ever spawned — reported as `AGENT_EXECUTION_FAILED`
   / `process_exit` instead of the auth, rate-limit or timeout failure the
   case scripted. Captured in the wild on the `hang_timeout` case
   ("Claude native Child behavior observation requires advertised
   --forward-subagent-text support.", 9 events, no spawn) and reproduced
   deterministically against `buildArgs`: old help text throws, derived
   help text builds args containing the flag.

   The fake's help text is now derived from `claudeAgentDef.capabilityFlags`,
   so it cannot drift from what the daemon probes for and the Run always
   reaches the scripted failure regardless of mapping order.

   This gate pre-existed the task-hierarchy change — the same fake and the
   same daemon check are on `main`. But that commit gave every sub-case its
   own daemon and data root, which keeps the OD Next rollout active for all
   five instead of only the first, so exposure went from one Run per file
   run to five.

2. The events log was sampled at an arbitrary moment. `GET /api/runs/:id`
   publishes terminal status before the buffered `events.jsonl` write has
   necessarily reached disk, so re-deriving a classification from that file
   could see a log whose error record was still in flight.
   `readCompletedRunEvents` waits for the log's own completion marker — the
   `end` record the daemon writes last, before closing the stream — rather
   than for a fixed delay or for the assertion to pass.

Each case now also asserts the daemon's own settled verdict
(`run.failureCategory` / `failureDetail` from the terminal status), which is
what the chat UI, the persisted message and telemetry actually consume. The
one case rejected before the runtime finish path declares
`daemonPublishesVerdict: false` rather than tolerating a null.

Verified under load average ~45-50 on a 10-core box (22 spinners plus a
concurrent full daemon suite): case 1 alone 6/6, file + tests/observability
6/6, file + observability + telemetry-message-finalization +
prompt-telemetry 5/5, every case in isolation 4/4.

* refactor(strategy): reshape the OD Next Prompt Bundle to the PRD layout

The emitted v2 bundle wrapped the cache-stable head in a `system_prompt`
element the PRD never asked for, and named three blocks differently from
the spec. Adopt the PRD-literal naming with the wrapper removed:

  open_design_prompt_bundle
    open_design_core_system_prompt   (was system_prompt/core_system_prompt)
      execution_boundary / native_execution
      discovery_and_planning_surface / core_strategy
      output_contract / echo_guard   (moved in: both are output constraints)
    session_skills
    active_stages
    task_metadata                    (was task_config)
    context
    user_first_prompt                (was user_prompt)

Block order stays load-bearing: the first three blocks are byte-identical
across tasks of the same strategy version, task type, and execution
profile, so the shared provider-cache prefix now runs through the end of
`active_stages` instead of `</system_prompt>`.

Three noise cleanups land with the reshape:

- `frozen_skill_package` and `request_input_facts` are omitted when they
  carry no substantive content, generalizing the rule the PRD already
  mandates for `attachments`. An empty Skill roster and an all-zero
  workspace/comment/MCP state cost ~540 bytes and told the model nothing.
- `recipe_identity` drops `strategy_package_hash`, `task_skill_digest`,
  and `stable_prompt_identity`. They are audit-only; observability and the
  persisted strategy task record keep them.
- Prompt text naming the old tags is updated: the echo guard constant, the
  form-override strings in both call sites, and the map notes.

Schema id `open-design.od-next-prompt-bundle/v2` is unchanged — it has
never shipped — and the v1 parser stays intact for persisted rows.

* test(daemon): make ingestion waits report what actually arrived

A bare "timed out waiting for the task observation owning run <id>" does not
say whether the daemon delivered nothing, delivered the compatibility
single-Run trace instead, or delivered the span under a different Run — so a
CI-only occurrence costs a reproduction cycle to classify. Both ingestion
waits now append the received trace/span/generation ids, names and runIds to
the timeout message.

Diagnostics only; no assertion, budget or behaviour change.

* fix(daemon): gate OD Next admission on what the installed CLI advertises

Making OD Next the default design strategy turned every prototype/ppt/
marketing/hyperframes Run with `claude` into an OD Next Run, and every
admitted Claude strategy Run asks the CLI for native Child behaviour
observation. `claude.buildArgs` refuses that unless the installed build
advertises `--forward-subagent-text`, and the refusal is a thrown
TypeError inside the spawn path, so the Run dies as
AGENT_EXECUTION_FAILED before the agent ever starts.

Admission never established that fact. `runtimeCapabilityVerified` was
derived from a bundled capability fixture keyed by runtime path + agent
id + adapter version, plus an `ensureDetectedRuntimeVersions` probe that
reads only `--version`. The fixture proves what the claude-code runtime
*path* can do; it says nothing about the build on the user's disk. So the
daemon admitted a task it could not launch.

The symptom hid on developer machines. A real Claude Code install had
already populated the process-wide `agentCapabilities` map through full
detection, and that stale entry answered "advertised" for a fake CLI that
advertised nothing — which is why four pre-existing daemon suites and one
e2e suite passed locally and failed on CI hosts with no Claude Code
installed. Ambient developer state was deciding the verdict.

Establish the fact at admission instead. `ensureDetectedRuntimeCapabilities`
runs the same bounded `-p --help` read full detection already uses, keyed
by the resolved launch path so a configured CLAUDE_BIN cannot be answered
from another binary's cached probe. An installed build that does not
advertise what OD Next will demand loses admission and takes the ordinary
route, reported as `od_next_rollout_capability_advertised_capability_missing`
with the missing keys on the `[od-next-rollout]` line.

`customAgents` is required alongside `forwardSubagentText` because a simple
task may escalate to complex after planning, and discovering that gap at
the production stage would strand the task mid-flight.

Red spec: apps/daemon/tests/od-next-advertised-capability-admission.test.ts

* fix(daemon): let a re-entered turn resume its own session

latestCompletedAssistantMessageId excludes the caller's in-flight assistant
placeholder so it cannot count as conversation advancement. Its
resumableMessageId parameter exists to admit the stored session cursor back
through that filter — but the exclusion was evaluated first, so when the
placeholder IS the cursor the row was already gone and the admission never
applied.

That collision happens when a daemon-internal restart re-enters
startChatRun with the same assistant message after that message was already
persisted as the session cursor: post-tool recovery writes the cursor, and
nativeSessionContinuePending is consumed once, so a later safe-retry restart
arrives without it and falls through to the ordinary guard. The live session
was then abandoned and the whole transcript re-seeded. For an OD Next
continuation it is worse than a slow turn — the non-request stage refuses to
cold re-seed and blocks the task instead.

The exclusion now yields to the admission for exactly that row. Advancement
detection is untouched: any later succeeded message still wins on
position DESC and still fails the cursor comparison, which a new test pins
so the fix cannot later be simplified into an unconditional cursor match.

* fix(web): hide internal OD Next rollout controls

* fix(daemon): stop OD Next from discarding delivered work

Seven defects found by running the four OD Next runtimes end to end. Each one
silently threw away a turn that had actually done its job.

- OpenCode wrote deliverables outside the project. It resolves its workspace by
  walking up to the nearest enclosing git root rather than using its process
  cwd, so a managed project directory — never a repository, and in a
  development install nested under the checkout — made it adopt the whole
  repository. The project stayed empty, artifact detection saw nothing, and the
  Run reported `no_artifact`. The BYOK definition already carried this
  workaround; the native one was missed. Sent unconditionally like BYOK does:
  gating it would turn a missing flag back into silent data loss, where an
  OpenCode build without `--dir` should fail loudly at spawn.

- The `--help` capability probe read only stdout. OpenCode writes its entire
  help to stderr, so every one of its advertised flags resolved to `false` and
  the capability mechanism was inert for that runtime — `--dir` above, and
  `--dangerously-skip-permissions` on builds that support it.

- Optional argv flags were dropped on a cold daemon. `agentCapabilities` was
  only warmed by full detection, so a turn that spawned before Settings or
  `/api/agents` built argv from an empty map. Resolve the advertised flags on
  the run path first.

- One ordinary simple Run could disable OD Next daemon-wide. A child-evidence
  coverage gap latched the rollout stop regardless of execution mode, but only
  the complex production gate consumes per-child coverage; a simple task has no
  Child agents, so `unavailable` there is accurate missingness. Recovery needed
  an operator `od strategy rollout reset`.

- A Direct Edit that delivered but never declared it was refused. The agent
  writes the canonical deliverable, `validateRunDeliverable` resolves it, and
  then the turn carries no machine block at all. No repair could rescue it:
  serialization repair needs a recovered Plan Contract to anchor on. Infer the
  completion from delivery Open Design verified itself, fail-closed on every
  weaker signal.

- A production block named nothing. Every other blocking path persists its
  reason codes; this one did not, so the most common production block reached
  the client anonymous.

- The web client remapped a blocked verdict onto a Run that had succeeded and
  delivered, hiding the artifact behind a generic failure card.

Also: tell the request stage the canonical-deliverable rule that judges a
Direct Edit completion. It shipped only in the production continuation, so
Direct Edit agents were graded on a contract they were never given.

Blocked diagnostics now report which machine blocks the turn actually emitted,
because "malformed block" and "no block at all" have different remedies and the
reason code alone cannot tell them apart.

* feat(daemon,contracts): expose which turn a strategy Run belongs to

A Full Plan task runs as several physical Runs (request -> production) that
the user asked for once, and the daemon-issued continuation carries no user
prompt of its own. Clients had no way to tell that those Runs are one
conversation turn, so the continuation surfaced as an answer nobody asked
for.

The daemon stays the single writer of one message per Run. It now also
reports the logical-task position alongside each message
(strategyTaskExecutionId + strategyTaskRunIndex, resolved in bulk from the
Run mapping) and emits a strategy_task_continuation diagnostic before the
source stream closes, so a live client learns the turn is still open. The
SSE diagnostic channel is contract-typed for the first time — the daemon has
always emitted it, ChatSseEvent just never declared it.

The read-side projection deliberately bypasses the record parser: it is for
display, so it must stay cheap across a conversation and must never let one
unverifiable task record fail the whole message list. Existing conversations
resolve their history without migration.

* fix(web): render a Full Plan task as one conversation turn

Each physical Run of a strategy task arrived as its own assistant message,
so a Full Plan answered one question with two turns: the planning Run
finished mid-answer — author line, "completed", copy/branch/rate actions —
and the production Run then opened a second turn that no user prompt
preceded.

The fold happens purely in the view. Messages sharing a task execution id
are folded into the turn's first message in Run order, appending content,
events and produced files, so nothing is dropped and nothing is repeated.
The turn reports the latest Run's status: an intermediate Run finishing is
an internal step, not the turn ending. Direct Edit (a single Run) and
ordinary runs are returned untouched.

Folding in the view rather than by re-pointing a message at the successor
Run is deliberate — the daemon owns message writes, and a client that also
re-points would have the continuation's output persisted twice.

* fix(daemon): let every runtime finish an OD Next complex task

Complex production was reachable for Codex, native OpenCode and AMR but
finishable only by Claude, and the refusal arrived after a full production Run
had already been spent — then latched OD Next off for the whole daemon.

- Scope Build Package ownership to the runtimes that can answer it. Completion
  demanded `attributes.buildPackageId` on every Child, but ownership rides on
  Claude's `--agents` / `subagent_type` transport and only that adapter stamps
  it. Assertions a runtime cannot answer are now skipped for it while every
  lifecycle, graph, parent and ordering assertion still applies to all four.
  This also makes those lifecycle checks reachable at last: a failed package
  check used to `continue` past the started/terminal verdict for that Child.

- Publish child-evidence coverage for ACP. The AMR branch emitted none, so
  every AMR task aggregated as `child_lifecycle_unavailable_not_zero` — unable
  to distinguish "no Child agents" from "nobody was observing". The consumer
  already accumulated bindings, negotiation state and rejection reasons; it now
  summarizes them, and reports `unavailable` rather than `complete` when the
  capability was never negotiated. The branch had no child-close handler at all.

- Promote native OpenCode child evidence to L2. The live root stream filters
  child sessions out, so it can only yield a terminal-only candidate that fails
  the evidence graph on `child_started_missing` and refuses every complex task.
  `opencode export --sanitize` is the one surface pairing a child's parentID
  with its transcript, and it can only be read once the child has exited. Half
  a lifecycle is worse than none — it adds an id that fails the graph while
  displacing the L1 candidate carrying the bounded Prompt — so a partial or
  unavailable export leaves that Child wholly on L1.

- Recover an undeclared production completion. A production turn is only ever
  entered from a locked Full Plan and its schema admits no non-terminal
  outcome, so a turn that ran the frozen plan, delivered a canonical entry Open
  Design resolved itself, and then answered in prose has exactly one thing it
  could have declared. Refusing it discarded a finished multi-page deliverable
  already sitting in the project.

Two diagnosis surfaces that hid the above:

- A schema failure reported only Zod's first message, so an absent required
  field read as the bare word "Required" with no way to tell which field the
  agent omitted. Carry the path.

- Repair-anchor recovery only matched a block that was nothing but a fence, so
  an agent narrating either side of its JSON produced no anchor and the one
  allowed serialization repair could not engage. Recover the first balanced
  object too; it still has to satisfy the full schema and is still only ever an
  anchor, so a wrong extraction yields nothing.

* fix(daemon,contracts): judge Build Package ownership by the evidence

Complex completion asked whether the selected agent was Claude, then skipped or
demanded `attributes.buildPackageId` on that answer. Ownership is a best-effort
attribute riding on Claude's `--agents` / structured `subagent_type` transport,
so an agent allowlist gets all three of its edge cases wrong: a runtime that
starts stamping ownership stays unverified and its real evidence is discarded, a
Claude turn whose model slipped is refused outright and latches OD Next off for
the whole daemon, and the list needs maintaining for every future runtime.

Ask the evidence instead. Ownership is asserted when every Child observation
names a package and skipped when none can, so verification follows capability
without anyone maintaining a list. Every lifecycle, graph, parent and ordering
assertion applies either way.

The binding factory keeps its own `claude` check: constructing `--agents`
handles is transport-specific and genuinely belongs to that runtime, which is a
different question from whether recorded evidence can be verified.

Also tell the Agent whether the runtime has the verified structured native Child
lifecycle that complex mode requires. The core strategy makes it a precondition
for locking complex, and the capability snapshot already answers it, but only
its hash reached the prompt — leaving the Agent to judge a capability it cannot
observe.

* fix(contracts): teach the plan contract the shapes only a complex plan emits

The canonical Plan Contract example is a simple plan, and the schema rejects a
simple plan that declares Build Packages, so that example necessarily carries
`buildPackages: []`. The prose beside it spells out the element shape of
`buildRequirements` and `readinessArtifacts` but never of `buildPackages`,
leaving the one array unique to complex mode with neither a template nor a
description. A model asked for complex had to invent seven `.strict()` field
names: codex and opencode independently guessed `dependencies` plus a stray
`boundary`, and both terminated on
`od_next_protocol_plan_contract_invalid_schema`.

The same single-item example hides a second rule. It makes
`canonicalDeliverable.id` equal the sole `requiredDeliverables` id, so the
membership invariant reads as a coincidence rather than a constraint. A complex
plan declares one deliverable per page and picks one as canonical, at which
point codex emitted a canonical id that appeared nowhere in the list.

Describe both in the output contract, next to the element shapes already
documented there. With this, codex and opencode both reach
`executionMode: complex`, and opencode carries a complex plan through to
`outcome: completed` with three native `task` children and
`child_evidence_coverage_v1` reporting complete coverage of all three.

* fix(daemon): name the codex rollout root so a complex Run can see its Children

`collectCodexChildEvidence` reads native Child lifecycle out of
`<CODEX_HOME>/sessions/YYYY/MM/DD/rollout-*.jsonl`, and deliberately refuses a
homedir fallback so it can never attribute one install's sessions to another.
That leaves the caller owing it an explicit root — and no caller ever supplied
one. `spawnEnvForAgent`'s codex branch was a pure pass-through, so the
collector's `CODEX_HOME` guard was false on every default install and the
collector has been unreachable since it landed.

The visible symptom was a complex codex Run that locked `executionMode:
complex`, spawned three native sub-agents, delivered four valid files, and was
then refused certification for child evidence the daemon had never gone
looking for. The rollouts were on disk the whole time, with each child
declaring the parent thread id.

Backfill the root before launch, mirroring the AMR `HOME` and
`OPENCODE_TEST_HOME` backfills next to it. The collector's no-fallback
invariant is preserved: the caller still names the exact root it hands the
runtime.

* fix(daemon): report unobserved Children as missing evidence, not malformed

`resolveDaemonOwnedOdNextComplexRuntimeEvidence` always brackets the Child list
with a running/completed root pair, so the empty-observation branch in the
complex gate is unreachable in production: a Run whose Children were never
observed still arrives with two perfectly valid observations and falls through
to the graph check, which reports `od_next_complex_child_evidence_invalid`.

That sends whoever debugs it hunting a corrupt payload. In the case that
prompted this, nothing was malformed and nothing was observed — the collector
had never run at all. Name the two states apart so the reason code says which
one happened.

* fix(daemon): stop one corrupt task record from voiding a whole boot's terminals

Reshaping the OD Next Prompt Bundle changed the v2 child tag set but kept the
schema id `open-design.od-next-prompt-bundle/v2`, on the reasoning that v2 had
never shipped. That holds for releases and not for a developer's own daemon:
the previous v2 composer was HEAD long enough to persist rows that still claim
today's label over a layout its parser cannot read. Those rows raise a raw
`TypeError` out of `parseOdNextPromptBundleV2`.

Startup reconciliation called `reconcileStrategyTaskRunTerminal` unguarded, so
that TypeError escaped `reconcileDurableRunTerminals` entirely. One unreadable
record therefore skipped the task-fact seeding, the whole PostHog `run_finished`
and Langfuse replay loop, and the task-observation crash-window sweep — for
every other Run on that boot, not just its own. Twelve consecutive boots of a
local daemon lost their terminal telemetry this way, and it only stopped when
the data directory was wiped.

Absorb the failure per Run, the way `strategyTaskTurnsForRunIds` already does
for the message list: a task row nobody can read is that Run's problem alone,
and startup still owes every sibling its terminal obligation.

* test(daemon): expect the pinned workspace in the OpenCode connection argv

Pinning OpenCode's workspace with an unconditional `--dir` also reaches the
connection-test probe, which spawns in a per-probe temp directory. That is the
intended reach — a probe must not adopt the repository root as its worktree
either — but the argv assertion was still written against the pre-`--dir` shape
and had been failing since.

Assert around the minted path instead of removing the check: the fixed prefix,
the flag, an absolute value, and the remaining flags in order. The 1.3
compatibility this test guards is a property of the argument order, so the
order stays asserted byte-for-byte on both sides of the temp path.

* fix(daemon): let an agent probe end even when the CLI swallows SIGTERM

`execFile`'s `timeout` is not a settle guarantee. On expiry it only *sends*
`killSignal`, and the promise settles on `'close'` — which needs the child to
actually exit. An agent CLI that traps SIGTERM and keeps a handle open leaves
the probe pending forever, and no caller's `catch` ever runs, so the
degrade-on-probe-failure path every caller already has is unreachable.

That became a hang on the Run path once `startChatRun` began awaiting
`ensureDetectedRuntimeCapabilities`, so the argv it builds matches a daemon
that has served `GET /api/agents`. The probe's cache key includes the resolved
launch path, so a fresh binary always misses and always probes. Against a
SIGTERM-trapping CLI the Run then parked in `queued` roughly 490 lines short of
`run.status = 'running'`: never spawned, so the inactivity watchdog never
armed, so it never reached a terminal status either.

Kill probes outright instead. They are read-only metadata calls with nothing
to flush, and every caller already treats a rejection as "probe failed, use the
safe baseline". `killSignal` is listed before the option spread so a caller
that needs a gentler signal can still ask for one.

* fix(daemon): stop discarding a codex Child that its parent re-invoked

A Codex sub-agent does not run once. The parent re-invokes it, and every
invocation opens another turn in the Child's own rollout, so `started` followed
by N-1 `interacted` is the ordinary shape of one delegated Build Package. The
collector treated a Child with more than one own turn as ambiguous and dropped
it, which meant a complex Run delegating three packages had all three Children
discarded and reported `knownChildCount: 0` with
`availability: partial` — while three complete Child lifecycles sat on disk,
each declaring the parent thread id.

The guard was defending something real: terminals are read from the parent's
activity list, so handing every turn the whole session's activities would let
one parent record terminate them all and stamp them with a single `endedAtMs`.
Slice the activities by the turn they occurred in instead — the parent's
records align with the child's turn starts within tens of milliseconds — so
each turn reads its own terminal, and a turn the parent never closed falls back
to the terminal its own rollout recorded.

Known gap, left for a follow-up: `knownChildCount` is keyed per (session, turn),
so it now reports one entry per invocation rather than per sub-agent. It feeds
the coverage diagnostic only — the complex gate counts observations — but it
makes codex's figure incomparable with the per-child count OpenCode reports.

* feat(strategy): port spec normative content into OD Next assets (#7141)

* feat(daemon): admit Vela to the OD Next capability registry

Vela drives the same native OpenCode runtime this registry already admits under
`native-opencode`. Its Child mechanism is that runtime's, reached over the ACP
extension instead of the CLI stream, and its fixture passes all seven evidence
paths. Holding the tuple out therefore never withheld an unproven capability —
it refused OD Next to an agent on the separate grounds that the build is not
yet immutable, which left every AMR run on the legacy plugin with
`od_next_rollout_x1_capability_fixture_unverified`.

Admit it. Re-pin `agentCliVersion` here once Vela publishes a build reporting a
stable producer version; that is the condition the old comment was really
waiting on, and it is a version pin, not an admission question.

Verified against a live daemon: AMR now resolves `od_next_rollout_eligible` /
`active`, and a simple full-plan task runs through OD Next to
`outcome: completed` with a valid deliverable. Complex still blocks for a
separate and honest reason — the installed Vela advertises no
`child_agent_lifecycle` producer, so no Child can be observed at all — which is
a Vela-side capability, not a gate.

* fix(daemon): stop one runtime's unverifiable Children from disabling OD Next

The rollout stop latch is `daemon_instance`-scoped and survives restart, so it
must be reserved for failures of the machine contract itself — those are true
whichever agent hit them. Unverifiable Children are not: they are a property of
one runtime. Vela ships no child-lifecycle producer today, so an AMR complex
Run cannot be certified at all, and every one of them took OD Next away from
Codex, Claude and OpenCode until an operator ran `od strategy rollout reset`.

The same latch also fired after a SUCCESSFUL run. `availability` demotes to
`partial` on any limitation, and a Codex collection always carries
`codex_inherited_turn_excluded` — children replay their parent's turns — so a
complex task that completed and delivered still disabled the strategy behind
itself. This was observed on a live daemon: the task reached
`outcome: completed`, and the rollout latched `complex_child_unverified`
seconds later.

Child evidence now raises no daemon-wide signal. That task is already
fail-closed with its reason codes persisted, which is the proportionate
response; route/mode drift and machine-contract leaks still latch.
`complex_child_unverified` stays in the reason-code union so daemons already
holding that latch, and the records explaining them, keep parsing.

* fix(daemon,contracts): count Child agents, not their parent's re-entries

`knownChildCount` had no stated unit, so each adapter picked one. OpenCode
counts distinct Child ids; Codex identifies an observation per (session, turn)
and the count was derived from observation ids at the call site, so a Child its
parent re-entered was counted once per invocation. A run with three sub-agents
reported four, and the two runtimes' figures were silently incomparable.

Say the unit in the contract — distinct Child agents, not invocations — and let
the Codex adapter own its own figure, the way the OpenCode adapter already
does. The per-turn observation identity is unchanged: it is what keeps each
invocation's lifecycle separate, which the complex gate still reads.

Verified on a live codex complex run: three native sub-agents now report
`knownChildCount: 3`.

* fix(daemon): stop inferring a complex completion nobody verified

The production-completion inference recovers a task the agent finished but
failed to declare, resting on Open Design having resolved the evidence itself.
For a simple plan that evidence IS the canonical deliverable, so the inference
is sound. A complex plan additionally owes verified native Child lifecycle —
the property that makes it complex — and no deliverable check substitutes for
it.

Accepting any locked execution mode therefore certified Children nobody
observed. Caught on a live AMR complex run: the installed Vela advertises no
child-lifecycle producer, the collector reported `knownChildCount: 0` with
`availability: unavailable`, the turn declared nothing, and the task landed
`outcome: completed` — walking straight past
`evaluateOdNextComplexChildEvidence`. The same run blocks correctly once the
inference is confined to simple.

* docs(daemon): record that Vela's Child producer is verified end to end

A local Vela built from the unmerged child-evidence branch negotiates
`vela.opencode.child_agent_lifecycle`, and an OD Next complex task on AMR now
reaches `outcome: completed` with `availability: complete`,
`source: vela_opencode_acp`, `knownChildCount: 3` and no limitations — three
native OpenCode sub-agents, one per Build Package, each observed through the
ACP extension. `verifiedRuntimeSupport` is the flag that makes exactly that
claim, so it is no longer false. `published` stays false: the producer is not
on Vela's main, so no user can install a build carrying it.

Also write down why this needs no version gate. Child evidence is reached by
capability negotiation, not by sniffing `agentCliVersion`: a Vela without the
producer never advertises it, so a complex task blocks on honestly missing
evidence, and one that has it works the moment it is installed. Admitting the
tuple therefore cannot make an older Vela claim Children it never reported,
and nothing has to be walked back when the producer ships.

* fix(strategy): default prototype demo content to real referents

Field test on beta.104 (claude, active, completed): the model read the
authentic-imagery clause, then legitimately routed around it by choosing
fictional demo books with CSS-drawn typographic covers — the clause is
conditional on content referencing a real entity, and nothing constrained
that upstream choice. The user-intended behavior (real books with real
fetched covers, for texture and credibility) never triggered.

Close the loophole with an affirmative default: prototype sample data
defaults to real, well-known referents with their real fetched images;
fictional stand-ins are a disclosed exception (user request or licensing),
and de-realizing content to avoid the fetch is named and forbidden. Core
baseline gets the matching one-liner so the default reads as baseline
discipline, not a profile-local whim.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(od-next): stop offering to continue work the task already delivered

A finished Full Plan turn could read as unfinished, offer to continue itself,
and have that offer end in a second task that could only fail.

A Run carries two accounts of whether its declared work is done. OD Next's own
verdict reaches `completed` only after the coordinator confirmed BOTH a
succeeded process AND a canonical deliverable resolved on disk — the entry file
exists, is readable, was touched by this Run, and matches the project kind. The
other account is the agent's last TodoWrite snapshot, unverified narration that
agents routinely leave with a stale `pending` item after writing the artifact.

The snapshot was winning. QA saw a task settle `completed` with seven artifacts
delivered, stamped `endedWithUnfinishedWork`, offering "continue remaining
tasks"; taking that offer opened a second task with nothing left to write,
which validated as `no_artifact` and settled `blocked`. One project then held a
completed and a blocked task for the same work.

Let the verified verdict outrank the narration, through one predicate shared by
the daemon's terminal classifier and both surfaces that read the snapshot — the
pinned todo card's continue affordance and the per-message footer. A
mid-generation truncation stays an independent term, so a turn cut off by
max_tokens is unfinished whatever verdict was recorded.

The verdict also has to survive a reload. The messages table has no strategy
column, so it lives only in the task store, and the client's projection probe
skips a task that is already terminal — reopening the project brought the bogus
offer straight back. The conversation read path already joins the task store to
place each message in its logical turn; it now carries the verdict along the
same join.

Watching a real Full Plan run in Chrome surfaced a second defect on the way in.
The live stream re-points ONE assistant message at each successor Run, so that
message accumulates every stage's output, while the daemon persists one message
per Run. On the post-run refresh the two views meet and
`mergeServerMessageWithLocal`'s "a longer local body is fresher" rule (#6396)
reads the accumulation as freshness and keeps it, beside the successor's own
row: the production answer rendered twice, live, until a reload cleared it.
Scope that rule to messages with no later sibling in the same task — the final
Run has nothing to have absorbed, so the guard is untouched for it and for
every ordinary turn.

Deliberately unchanged: `artifactCount <= 0 -> no_artifact` still blocks. That
guard is what stops a turn claiming completion it never delivered, and the
recovery case QA hit disappears once the offer stops appearing on finished
work. A turn that legitimately has nothing left to write needs a "no change
required" outcome in the protocol, which is a product decision, not a defect
fix. Note also that a settled task cannot be resumed — the daemon rejects any
continuation of a terminal task with 409 — so a continue affordance has no
choice but to open a new task.

Also drops `onStrategyTaskContinuation`, a client callback with no consumer
whose contract described re-pointing the existing message at the successor Run
— the very shape that prints the answer twice. The daemon still emits the
diagnostic into the Run event log, where it documents the hand-off for anyone
reconstructing a multi-Run turn.

Red first: the end-to-end spec fails on the pre-fix daemon with
`endedWithUnfinishedWork` true, again with the reload verdict `undefined`, and
the merge spec with the production answer counted twice.

* fix(web): stop leaking the strategy package above a user prompt

An OD Next turn opened with two rows of chrome the user never asked for: a
"Design" session-mode chip and a "Using OD Next Strategy V2" disclosure that
named the internal strategy package and its version. Neither is context the
user attached to the message — the daemon binds the strategy package itself,
and the mode chip only restates the default that a strategy-owned design turn
already implies.

Key the suppression on `AppliedPluginSnapshot.strategy`, which the daemon
stamps only for an internal strategy apply (verified against real persisted
snapshots: OD Next rows carry `strategy_json`, ordinary plugin rows store
NULL). A user-picked plugin, skill, design system or workspace reference still
renders exactly as before, and Ask/Plan stay labelled everywhere because those
are per-turn behaviour choices the user made.

An OD Next design turn now opens with the prompt itself.

* fix(strategy): add action-level layout mechanics to the baseline

Field test on beta.105 (first AMR active sample, deepseek-v4-flash,
prototype completed): the artifact shipped two elementary geometry
defects — the city switcher and daily-judgment regions overlapped, and a
stat numeral's percent sign wrapped to a second line inside a fixed-size
ring. The existing rules could not have prevented either: they are
outcome-level ('overflow-free layout', 'no overflow or clipping risk'),
and under ship-on-write the model never sees the rendered outcome it is
asked to guarantee.

Replace the bet on outcomes with rules about the source itself — verifiable
while writing: flow-only layout for sibling content regions (no absolute
positioning / negative margins / transforms to stack them), fixed-size
boxes only for fixed-size content, clamp()-sized display numerals with the
unit pinned to the same line, block-level display declared before sizing
track-style elements, no hidden-overflow masking, no 1–2 character
orphans. Core gains the Layout mechanics baseline bullet; the prototype
quality floor names the two observed failure shapes.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(strategy): add measure-then-size image geometry to the baseline

Follow-on field sample: a fetched, localized movie poster rendered
cropped inside a container whose fixed ratio the model invented — a
failure class created by the real-first imagery clause itself, since
fetched assets carry intrinsic dimensions the model never authored.

The fix is procedural, not aspirational: read the file's intrinsic
width/height during Build (input probing sits inside the ship-on-write
allowance), give the container the measured ratio (aspect-ratio or
natural flow), reserve cover-cropping for decorative backdrops, and
render content-bearing images — posters, covers, artwork, product
shots — at their full frame with no double-locked axes. Core gains the
Image geometry baseline bullet; the prototype quality floor names the
observed failure shape.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(strategy): anchor overlays on the anchoring axes only

Address PerishCode's review: 'explicit inset on every side' would, taken
literally, set all four offsets on an auto-sized absolutely positioned
badge and stretch it to fill its containing block — the exact malformed
overlay geometry this baseline exists to prevent. Anchor with offsets
only on the anchoring axes (top plus right for a corner badge), size
constrained independently, and name the stretch trap explicitly.

* revert(strategy): split image geometry out of this bugfix

Per review: the measure-then-size image-geometry policy addresses a
separate field failure (localized poster cropped by an invented fixed
ratio) and belongs in its own focused PR with that reproduction. This
branch returns to its stated objective: region overlap, stat-unit
wrapping, and track sizing.

* fix(strategy): add measure-then-size image geometry to the baseline

Field reproduction (beta.105, od-next active, prototype route): the
real-first imagery clause now makes runs fetch and localize real assets
— and the first such sample shipped a localized movie poster rendered
cropped, because the model invented a fixed container ratio for an
image whose intrinsic dimensions it never authored, then let cover-crop
hide the mismatch. The page showed a partial poster frame: wrong ratio,
content cut off. This failure class is created by fetched assets
carrying dimensions the model has not seen, so it cannot be prevented
by the existing outcome-level rules.

The fix is procedural and write-time-verifiable: read the localized
file's intrinsic width/height during Build (input probing sits inside
the ship-on-write allowance), give the container the measured ratio
(aspect-ratio or natural width:100%/height:auto flow), reserve
cover-cropping for decorative backdrops, and render content-bearing
images — posters, covers, artwork, product shots — at their full frame
with no double-locked axes. Core gains the Image geometry baseline
bullet; the prototype quality floor names the observed failure shape.

Split out of #7169 per review scope discipline.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(strategy): allow intentional fixed and sticky chrome

Address PerishCode's review: 'normal flow only' contradicted the
prototype profile's own environment rules, which expect fixed headers,
bottom navigation, and floating controls that reserve matching padding —
a model following the core rule literally could strip valid mobile
bottom navigation. Narrow the prohibition to its actual target
(accidental stacking of sibling content regions) and state the chrome
allowance with its padding obligation in both the core bullet and the
prototype quality-floor line.

* fix(web,daemon): keep internal turn chrome and control markers out of the chat

Two acceptance findings, both the same shape: daemon-internal plumbing reaching
the reader.

The session-mode chip flashed "Design" above a freshly sent prompt and dropped
it a beat later. The optimistic user message renders before POST /api/runs
answers, so the strategy binding the previous fix keyed on simply is not there
yet. Key the chip on the mode alone instead: it labels a turn the user steered
away from the default, so Ask and Plan stay labelled and Design — the mode every
design project already runs in — carries no chip. That is decided the moment the
message renders, with no snapshot to wait for.

`<od-title>…</od-title>` rendered as message body. The stripper was armed only
for Runs that requested a title, and the web only requests one on the first turn
of a conversation, so from turn two onward nothing consumed the marker while the
directive was still sitting in the agent's own resumed session history. Arm the
stripper on every Run and swap only the announce callback, then strip again in
the chat: the OD Next machine protocol blocks join the title marker there, and
messages already persisted with a leaked marker render clean, which no
daemon-side fix can reach. Renderable artifacts (question-form, od-card,
artifact) are deliberately excluded.

* fix(web,daemon,strategy): stop a stray question-form marker from stalling a turn

An OD Next turn that needed no clarification narrated its decision straight
into the marker — `<question-form> 无需提出——…`, an unclosed tag whose body is
prose. Four surfaces independently read "an open marker is present" as "a form
is being generated", at three different strictness levels, and none of them had
a state for markup that can never render:

- the web parser raised a loading skeleton and truncated every character after
  the marker, so the turn's real output was invisible;
- `hasPendingQuestionForm` held the turn in a clarification handshake;
- `resolveDesignDeliveryOutcome` returned `awaiting_input` before looking at
  what the run had delivered;
- `listProjectsAwaitingInput` matched the marker with a raw SQL `LIKE`, latching
  the project on `Needs input` until the user typed something.

The daemon's own strict detector meanwhile scored the stray marker as zero
forms, so the coordinator accepted the turn with an empty reason-code list. The
one component that could have reported the contract break was blind to it by
construction.

Give every one of those decisions the same precondition — an open marker only
means a form is in flight while the text after it could still parse as the form
body — and make the malformed case reportable instead of silent. The two new
coordinator codes are attribution, never a gate: every code `validateAcceptedTurn`
returns blocks the turn, so a stray marker on an otherwise valid planning turn
still reaches production.

The awaiting-input queries needed care. They take the newest form-bearing
message per partition and test only that one for an answer, so the strict check
has to run before the newest-per-partition pick. Post-filtering the winning rows
would let a stray marker win its partition, fail the check, and drop a project
that is genuinely waiting on a real unanswered form. Both directions of that
ordering are pinned by specs, and the scan streams without retaining message
content because it runs on every `GET /api/projects`.

The strategy prompt now also forbids restating the marker to announce that no
form is needed. Every clarification rule was phrased as "when to emit a form" /
"skip the form"; nothing forbade writing the bare marker as a label, which is
what produced the turn.

* fix(strategy): define the session-skill conflict order (#7188)

The Instruction order named the three session-skill layers but gave no
tie-break when they conflict inside their shared applicable scope, so a
user-selected skill and a bundled profile could pull the model in
opposite directions with no principled resolution. Make the order
explicit: user-selected skills first, then the general orchestration
Skill, then the task-type profile — and state that the tie-break never
unlocks what higher rules forbid (ship-on-write actions, the V2 machine
contract, the user's explicit requirements). Spec source of truth
updated with matching wording in both languages.

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* feat(od-next): restore anti-cliché visual rules across the strategy package

The od-next rewrite dropped the legacy anti-slop blacklist (core-slim /
discovery ❌ lists), leaving the package with no background-tone rules at
all — prototype.md even referenced 'the anti-cliché rules above' that did
not exist. Models revert to their warm-cream 'AI look' prior on every
task type.

- core-system-prompt.md: new cross-task 'Anti-cliché defaults' block in
  the Design baseline (warm beige/cream grounds, purple-gradient wash,
  stock UI display faces, left-border accent cards, icon-per-heading,
  gray hover, hand-drawn SVG people, invented metrics)
- prototype.md: resolve the dangling reference with a labeled rule
  cluster; add the designer/demo-controls ban
- ppt.md: new 'Avoid deck clichés' section (gradient-wash covers,
  icon-per-bullet, center-everything, corner blobs)
- marketing.md: cream/stock-gradient ground ban + never invent awards,
  badges, press quotes, or testimonials
- hyperframes.md: cream color-wash grade + particle/glow filler ban

Asset versions unchanged: package identity tracks content via sha256
digests; tests pin '2.0.0' literally.

* feat(web,daemon,contracts): carry an official example card into OD Next

Clicking an official example card under a task type pinned that example as an
executable plugin. The pin stamped explicit_user authority, which took the task
off the automatic route entirely — no strategy task, no plan, no clarification
round — while free text under the very same chip entered OD Next normally.

An example card is a more specific choice inside a task type, not a claim on
the strategy. So send its identity instead of a plugin pin: `exampleReference`,
which the daemon re-resolves through the same local catalogue lookup
`/apply-local` performs, reading the example's bytes from the resolved path and
never from the request body. Its SKILL.md and the side files its prose names
then travel where user-chosen material belongs — `session_skills/
user_selected_skills`, a bundle region that already existed in exactly this
shape and had no producer. No bundle format change, no schema bump.

The discriminator has to be the call site, not the record: the same
`InstalledPluginRecord` is simultaneously an example card, a chip's default
scenario plugin, and a Community entry, and nothing in its data tells the three
apart. `examplePick` is therefore set only by the example-rail handler, and a
genuine plugin pick from the Community page keeps its explicit authority. An
example card under a chip with no OD Next route — wireframe, mobile, image,
video — behaves exactly as before.

Drop the planning/Build-only guard from user-selected Skill capture. Measured
against real content it rejected most of the corpus it was applied to while
protecting almost nothing: it is 15 English regexes, so the same sentence
passes or fails on language alone — "After the build, run through the checklist
and fix any defects" is refused and its Chinese equivalent is admitted. It also
false-positives on prose that merely names the word, and it never inspected
side files at all, so a `references/checklist.md` always walked past it. 47 of
183 shipped examples tripped it, including all three default cards. Conflicting
instructions are resolved by the Instruction order the core system prompt
already defines; first-party strategy assets keep the guard, where the corpus
is controlled and the rule works.

Two latent bugs in that rail surfaced once it had a caller, both of the same
shape — one bad side file deleting an entire Skill. A prose link to a file that
was since flattened away (`example-hyperframes`) and a bundled screenshot past
the per-file byte cap (`example-open-design-landing`) each sank the whole
capture. Neither says anything about whether the Skill is safe to carry, so
both are skipped; a symlink, a symlinked parent, or a file that changes under
the read still fails closed. The byte cap keeps its purpose — oversized bytes
are never read — and `readBoundedNoFollow` still enforces it on what is read.

Answer the brief's template from the card's own input defaults. `od.useCase.
query` is a template that the ordinary route fills from the applied snapshot's
inputs; an example card deliberately creates no snapshot, so without this the
Agent received `{{artifactKind}}` and `{{audience}}` raw — a broken template
rather than an assignment. A field with no declared default keeps its token
instead of inventing an answer the card never gave.

Verified against the shipped corpus: 182 of 183 example folders capture and
serialize into a `user_selected_skills` region, the one exception shipping no
SKILL.md and belonging to no OD Next route. 102 cards across Prototype, Deck
and HyperFrames now enter the automatic route; the other 81 are untouched.

* fix(web,contracts): keep a nested Prototype scene on the OD Next route

Picking a second-level scene under Prototype — Mobile app or Wireframe — took
the task out of OD Next entirely, for free text and example cards alike, while
the first-level Prototype chip worked. Two independent layers excluded them and
cross-check each other fail-closed, so neither could be relaxed alone: the
composer swapped the route id to the scene's own catalog action id, which the
route table does not carry, and the metadata re-derivation separately rejected
`fidelity: 'wireframe'` and mobile platform targets.

Neither exclusion was protecting anything. The two scenes are the Prototype
route plus metadata, as their own chip definitions say — both bind
`example-web-prototype` with `kind: 'prototype'` and `automaticDefault: true`,
differing only by `platformTargets` or `fidelity`. And the Prototype task
profile already branches on exactly those two: it downgrades content and
interaction-state requirements when fidelity is wireframe or lo-fi, and it
carries mobile guidance down to touch reach and the 375/768/1024/1440
breakpoints. The route was being denied to the cases the profile was written
for.

A second-level scene refines WHAT to build, never WHETHER the parent task
type's route applies. Make that structural rather than enumerated: routing
resolves the first-level task type through `taskTypeChipIdForChipId` instead of
reading whichever catalog action the scene happens to bind, so a scene has no
route id of its own to route by and a scene added later cannot strand its
parent. Adding table rows would have fixed today's two and left every future
scene a silent-regression candidate.

On the contracts side `intent` is now the only field that can move a project
off a route — it is the only one naming a different pipeline. `fidelity` and
`platformTargets` stay in the signature to record that the decision has seen
them and chosen not to gate on them.

Deck and HyperFrames were never affected and now have witnesses that say so:
Deck's second-level rail is a card filter that never leaves the hero, and
HyperFrames has no rail at all. A catalog sweep pins the routed chip set to
exactly deck, hyperframes, mobile, prototype and wireframe, so Image, Video,
Audio, Document, Website clone, WebGL, Figma and the rest cannot join it
silently.

* refactor(web): make a nested Prototype scene a second-level type

#6914 moved Mobile app and Wireframe out of Home's first-level rail and into
Prototype's second-level scene rail — but only in the UI. Underneath, both
stayed full first-level chips with their own `apply-scenario` action, merely
hidden from the create rail, and the scene pointed back at them through
`actionChipId`. Its own comment recorded the compromise: keep their catalog
action ids so the nested scene preserves the metadata those actions stamp.

A thing that renders as a refinement of Prototype while still being a task type
in its own right is what dropped these two scenes out of OD Next: the composer
resolved the route from whichever action the scene bound, and Mobile and
Wireframe are not routes. `f360f46e0` stopped the routing site from asking.
This removes the ambiguity it was working around.

A scene now carries its own refinement — Mobile stamps the handheld platform
targets, Wireframe stamps the lo-fi fidelity — and binds its parent. The two
chips leave the catalog, because they were never task types. The refinement
type cannot restate its parent's `kind`, so a scene is structurally incapable
of becoming a type again.

Nothing the user sees changes: same eight scenes, same labels, same metadata
stamped byte for byte, same Prototype route, same example-card behaviour. The
integration assertion was tightened from `objectContaining` to an exact match
to say so, and the unit specs compare the serialized metadata rather than its
fields, since project metadata is persisted and compared as JSON.

Legacy ids still arrive from outside the catalog, so normalization moved to the
two boundaries where that actually happens — a persisted composer draft and a
queued one-shot chip intent — instead of living at the routing site. The queued
intent had no coverage and relied on `pickChip` normalizing before `findChip`;
it has a spec now. `taskTypeChipIdForChipId` is gone: with the scenes out of
the catalog, `active.chipId` is a task type by construction, and every producer
was traced to confirm none can yield a scene.

The placeholder carousel had the same latent shape — three scenario lines bound
a scene as if it were a chip — so they bind Prototype plus a scene id, and the
carousel narrows by scene slug. A one-click create from a scene's carousel card
now has a spec proving it keeps both the refinement and the route.

* fix(web): stop a restored composer pick from costing its task type

Dismissing the pill of an example card returned the Home composer to nothing —
no task type, no OD Next route, and the user's typed brief wiped with Send left
disabled — but only after the composer had been remounted. Opening a project,
visiting Settings, any EntryView swap: the composer comes back looking
pixel-identical, same type selected, same scene, same pill, same brief, and the
dismiss now destroys instead of stepping back.

The persisted chip draft records what was picked and never how. A restored
example card therefore comes back as an ordinary chip binding, while still
rendering its own pill and dismiss control — that check asks whether the record
differs from the chip's default plugin, which is true for every example card.
The dismiss handler keyed its step-back branch on the pick being explicit, so a
restored pick missed it and fell through to the reset that clears everything.

Persist how the pick was made and restore it, so a pick is the same object
after a remount as before one. Then key the step-back on the pick naming a task
type rather than on it being explicit: whatever a pick was, if it named a type
the user chose, dismissing it returns there. Only a pick that never named one
resets the composer, and the typed draft is left exactly as typed either way.
Drafts written before this carry no flags and restore exactly as they do today.

The same lost provenance had a second victim: an example card that was never
dismissed stopped sending its `exampleReference` after a remount, so the card's
own material silently vanished from the run while the route survived — on all
three OD Next routes.

The reproduction drives the real rails and the real bundled catalog through ten
first-level and second-level combinations, each on a fresh mount and across a
remount. The differentiator was never the type or the scene: every combination
loses its route once the pick's provenance has been through storage.

* feat(od-next): ship handheld device shells with the prototype task profile

Blind evals against the live slim-sp strategy showed OD Next losing every
phone-app prototype on one detail: the live route hands the agent a real
handset seed (design-templates/mobile-app), OD Next only described one in
prose and the agent drew a rounded card by hand.

- Declare task-profile `resources` in the bundled strategy contract; the
  prototype profile ships iPhone / Android / platform-neutral shells that
  enter the package identity with the profile (rule-card and shell edits
  move the hash alike) and are re-verified at consumption.
- Stage the shells into `<cwd>/.od-frames/` on every strategy run and
  add a "Handheld device shell" section to the prototype rule card with
  the brief→shell table and the shell invariants.
- Resolve the platform from project metadata and the user's own words
  (ios / android / mobile-neutral; responsive and desktop briefs veto the
  platform-less class), latch it per conversation, and quote the selected
  shell into the stable request context as `device-frame` +
  `device-frame-shell` facts.
- Observe at run finish whether the delivered entry kept the shell and
  expose it on run_finished (od_next_device_platform / _source /
  _shell_present) so the rollout can measure adoption.
- Shells collapse to a full-bleed phone page below 480px and scale on short
  viewports so the artifact still meets the 375px rule.

* docs(od-next): describe task-profile resources in the strategy package SKILL.md

* fix(od-next): stage device shells non-destructively and cover the collab exclusion

Review: `.od-frames/` is a name this PR introduces inside a user-owned
project directory, so wiping it on every run could delete pre-existing
content. Staging now keeps an ownership manifest
(`.od-frames/.od-next-device-frames.json`): only files it recorded earlier
are replaced or retired (and retired only while their bytes are still
ours); a pre-existing file under a managed name is left untouched and
reported as skipped; unrelated files are never read, written, or deleted.
Tests prove an unrelated file survives, a foreign `iphone.html` survives
repeated staging, and a user-edited staged file is not retired.

CI: `.od-frames` joins the expected member-mirror exclusion list in
`vela-cli-resource-adapter.test.ts`, which pins that list exactly.

* fix(od-next): prove device shell bytes are ours before replacing them

Review: listing a name in the ownership manifest was not enough to claim
it. After the first staging a user could edit `.od-frames/iphone.html`
and the next run's unconditional `writeFile` destroyed that edit, and a
pre-existing `.od-next-device-frames.json` under an unrelated schema was
read as an empty manifest and then overwritten.

Ownership is now provisional on both sides. A managed name is written or
retired only when the manifest claims it AND the bytes on disk still hash
to the recorded digest — the rule the retirement path already followed,
now applied to replacement too. A digest that moved makes the file the
user's: it is reported as skipped and drops out of the manifest, so later
stagings keep their hands off it. The control file gets the same
treatment: `readOwnership` classifies the name as absent / ours / foreign
instead of collapsing unreadable, malformed, and unrelated content into
an empty manifest, and a foreign control file makes the whole directory
off limits — nothing written, nothing deleted, every managed name
reported as skipped. In each case the quoted `device-frame-shell` fact
still carries the shell source, so the run itself is unaffected.

Two fixture cases, both red on the previous head: a shell edited after
staging survives repeated staging byte-for-byte, and a pre-existing
invalid manifest plus the user's own `iphone.html` survive untouched with
no shells created.

* fix(od-next): bound the device shell manifest to the names it can stage

Review: the manifest lives inside a directory the project controls, so
its key list was letting the project widen this feature's deletion set.
The parser accepted any slash-free basename with a SHA-256 digest, and
the retirement loop then deleted any accepted entry missing from the
current package while its bytes still matched. A project holding a
same-schema `.od-next-device-frames.json` that claims `leftover.html`
with that file's real digest therefore lost `leftover.html` on the next
strategy run — verified, not hypothetical: the new fixture fails on the
previous head with `ENOENT` on that file.

The bound is now this feature's own filenames rather than whatever the
manifest names. `MANAGED_SHELL_FILES` is derived from
`OD_NEXT_DEVICE_FRAME_FILES`, and a manifest is `ours` only when every
key is in that set and every value is a 64-hex digest — that is exactly
what this materializer writes, so anything else came from somewhere else
and the whole record is refused as foreign, leaving the directory alone.
Refusing beats salvaging the well-formed entries here: salvaging is what
would let a crafted entry keep nominating an unrelated file. The
retirement loop re-checks the allowlist as well, so the deletion set
stays bounded even if the parser is later loosened.

Manifests written by the previous head hold only shell names and valid
digests, so they keep reading as ours; no migration.

* fix(od-next): let an @-mentioned Skill ride the automatic route

Mentioning a Skill in the composer took the turn off OD Next entirely. The
project fell back to its task type's legacy plugin, so the chat header
announced "正在使用 Web Prototype" and the run reached the agent as ordinary
prose with no Prompt Bundle at all — no Runtime State, no Plan Contract, none
of the strategy the task type was supposed to own.

Three layers each dropped the Skill or the route, and every one of them had to
give for the turn to survive intact:

- The admission gate counted a named Skill as explicit user authority, next to
  a pinned plugin and a pinned snapshot. A Skill is not an executable surface
  and cannot run a strategy; it is material for one. It now stays out of that
  read, and a context plugin remains the only non-plugin signal that still
  claims the route.
- Home treated the task type and the Skill as mutually exclusive: picking
  either one wiped the other, and submit dropped whichever survived. That rule
  came from #2972, which asked for a defined answer when the prompt's intent
  and the picked card disagree. "The Skill wins, drop the card" is defined, but
  it routed a user who picked 幻灯片 into a prototype project, which is exactly
  the silent misrouting #2972 objected to. The task type now owns the route and
  the Skill is material carried inside it, so the conflict resolves without
  discarding either.
- The create handoff nulled `skillId` the moment a task profile was claimed,
  which would have swallowed the Skill one layer after Home stopped dropping it.

The catalogue capture entry point for exactly this already existed and had
never had a production caller — the gate above made it unreachable. It is now
wired through a single session capture that converges both channels a session
can select material through, because one task freezes ONE package: an
@-mentioned Skill and an official example card land in the same
`session_skills/user_selected_skills` slot, the mention first, which is the
authority order the strategy's own conflict rule already assumes.

Capture is fail-soft on every leg, matching the example-card rail: a Skill
deleted between picking and running, a catalogue that will not list, a
selection past the package bounds — each drops that material and runs the task
without it. The ordinary route drops an unresolvable Skill silently too, so
this is the same contract rather than a new leniency, and an unfreezable
selection can never cost the project the example card it was created from.

Skill-catalogue scoping moved to one helper shared with the system-prompt
composer. A Skill one surface can resolve is now a Skill the other can freeze;
resolving them from separately-derived partitions would drop a Workspace Skill
on whichever side guessed narrower.

Coverage: the three request shapes that carry a Skill (`ChatRequest.skillId`,
`skillIds`, and the Skill persisted on the project row by Home's create) each
have an HTTP witness asserting the Bundle text; all four routed task types are
covered for a mention arriving alongside their default card. Two server tests
that encoded the old "Skills route ordinary" contract, and the two Home specs
that encoded the mutual exclusion, were rewritten rather than deleted — each
now states the rule that replaced it.

* fix(od-next): accept a HyperFrames composition as its canonical deliverable

A HyperFrames run that wrote an editable root `index.html` was blocked with
`od_next_canonical_deliverable_invalid`, surfacing as "任务执行失败" even though
the artifact was sitting in the project and working. Every HyperFrames
production turn in the reported diagnostics bundle hit it.

`validateRunDeliverable` gated the entry's file kind on `metadata.kind`, and
`kind: 'video'` accepted only `video` files. But `kind` records which Home
surface a project was created from, not the shape of what it delivers. For
every other kind those coincide; HyperFrames breaks the identity — it is a
local HTML-to-video renderer, so the HTML composition is the authored artifact
and the MP4 is a render of it, produced on demand and not necessarily present.

OD Next already said so on the way in: `DAEMON_OWNED_OUTPUT_KINDS.hyperframes`
admits `html` and `source` as supported deliverable kinds at plan time, so the
same daemon approved a plan the completion check then rejected.

Resolve the accepted kinds from the project's metadata instead of its bare
`kind`, via one named invariant used by both entry inference and the kind gate.
Generative video (fal / Veo / Sora / Volcengine) keeps the strict video-only
contract: there the provider returns a video file, and an HTML preview page in
its place is a genuine delivery failure.

The `hyperframes-html` discriminator moves next to `ProjectMetadata` so
delivery validation and analytics `project_kind` read one constant.

Red spec: `apps/daemon/tests/run-deliverable-validation.test.ts` reproduced the
production `type_mismatch` before the fix, and covers both guards after it.

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: Cheems <cheems1229282@proton.me>
Co-authored-by: 黄桃 <huangtao@huangtaodeMacBook-Pro.local>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: hihijack111 <216108319+hihijack111@users.noreply.github.com>
2026-08-24 08:53:36 +00:00
lefarcenandGitHub c9eb23027f fix(viewer): preserve live HTML previews (#7156)
* fix(viewer): preserve live HTML previews

* fix(viewer): sync saved text to retained preview

* fix(viewer): prewarm retained file revisions

* fix(viewer): expire saved preview adoption

* fix(viewer): repaint retained previews on activation

* fix(viewer): reconcile retained style history

* fix(viewer): renew preview asset scopes without reloads

* test(viewer): cover scope renewal in team previews

* test(exports): expect managed preview bases
2026-08-21 11:41:27 +08:00
Ray XiandGitHub 0d53c96beb fix daemon HyperFrames scaffold and runtime (#7030)
* fix daemon HyperFrames scaffold and runtime

* fix CI snapshots and Nix hashes

* fix(pack): preserve HyperFrames sharp runtime

Copy the target-native sharp closure into final standalone payloads and execute the packaged HyperFrames CLI during afterPack.

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* test(pack): avoid chmod on linked Node fixture

Only chmod the copied fallback so a successful hard link does not try to change the CI-owned Node inode.

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
2026-08-19 03:16:46 +00:00
Andre RossandGitHub 33b9b6f1cb Add Handoff to OpenCode plugin (#6948)
* Add Handoff to OpenCode plugin

Registers handoff-od in the community catalog, pointing at its canonical
repository at github.com/ross-sec/handoff-od@v0.1.3.

Bumps the catalog version and metadata.version 0.2.3 -> 0.2.4.

* Repin community/handoff-od to v0.1.4

Addresses the review on #6948: the pinned v0.1.3 release had an empty-slug
deletion path. A project name written entirely outside [a-z0-9] slugified to "",
so join(outRoot, "") resolved to outRoot itself and --force ran
rmSync(outRoot, { recursive: true, force: true }) against it.

v0.1.4 fixes it in two places — a non-empty deterministic fallback slug, and a
guard that refuses any delete/write target which does not resolve to a strict
descendant of the output root — with regression tests for both. v0.1.3 and
earlier are deprecated on npm.

Catalog version 0.2.4 -> 0.2.5.

* Repin community/handoff-od to v0.1.5

Addresses review round 2 on #6948: the pinned v0.1.4 mirrored symlinks without
classifying them, so a link to a file outside the project copied that file's bytes
into the bundle, and a link to an external directory aborted the mirror with
EISDIR/EPERM.

v0.1.5 adds I9 — nothing outside the project directory is ever mirrored, named or
read — applied to the resolved target: internal links are followed and materialized,
internal-but-excluded links are skipped as their target would be, external and
dangling links are refused and named before anything is written or deleted. Raw path
input (--files, --focus, --entry, designSystem.dir) is checked the same way. Phase 03
asserts the finished bundle contains no symlinks at all.

Fixture coverage for file and directory links with targets inside and outside the
project, as requested. 54 tests, green on Linux and Windows. v0.1.4 and earlier are
deprecated on npm.

Catalog version 0.2.5 -> 0.2.6.

* Repin community/handoff-od to v0.1.6

Addresses review round 3 on #6948: depth=both never placed the authored spec in the
bundle. depth was a declared manifest input that phase 00 wrote into state and no
script read, and the documented order ran phase 01 before phase 02 — so the mirror
was snapshotted before the spec existed and nothing refreshed it.

v0.1.6 reorders the workflow so the authored spec exists before the bundle snapshot,
and enforces it: phase 01 refuses at depth=both until the spec is present and
TODO-free, phase 01 and phase 03 both gate the nested spec, and an end-to-end fixture
reads the finished archive's central directory to prove the spec README and its
prototypes are inside it.

The audit that review prompted found two further declared inputs that did not do what
the manifest advertised — includeChats promised a chats/ directory nothing created,
and transport=both emitted only one prompt. Both fixed, and I10 now asserts every
declared input is wired to behaviour and checked by a gate.

64 tests, green on Linux and Windows. v0.1.5 and earlier are deprecated on npm.

Catalog version 0.2.6 -> 0.2.7.

* Repin community/handoff-od to v0.1.7

Addresses review round 4 on #6948: the phase-03 verdict could outlive the bundle it
described. validate.json carried only ok/root/gates, so a --force rebuild or a
post-validation edit left phase 04 archiving a tree phase 03 never inspected —
skipping root purity, symlink rejection, spec completeness and adherence checks.

v0.1.7 clears the verdict before every bundle mutation and binds a successful verdict
to a content digest of the validated tree, which phase 04 recomputes and refuses on
any difference. Phase 04 additionally reads each archive back and compares its file
entries to the validated set rather than comparing counts, and refuses a bundle
containing symlinks the digest walk would not have seen.

Regression fixtures for both paths the review named: a --force rebuild and a
post-validation mutation each make archive creation fail until phase 03 runs again.

75 tests, green on Linux and Windows. v0.1.6 and earlier are deprecated on npm.

Catalog version 0.2.7 -> 0.2.8.

* Repin community/handoff-od to v0.1.8

Addresses review round 5 on #6948: phase 04 could produce a passing archive whose
bytes were never validated. v0.1.7 hashed the tree before invoking the external
archiver and then checked only the archive's entry names, so a file swapped in between
— by another process in the output tree, or a substituted tool — shipped unvalidated.

v0.1.8 records a per-file sha256 in the phase-03 verdict and has phase 04 decode the
finished archive and compare the hash of every regular file it carries against that
record, reporting missing, unexpected and altered contents separately. A failed
archive is deleted rather than left looking finished. Decoding is in-process so the
check does not depend on the same external tool family that wrote the archive.

Regression fixture as requested: a tar shim ahead of the real one on PATH mutates a
bundle file when phase 04 invokes it, then delegates, placing the mutation after the
pre-archive check and before the archiver reads.

77 tests, green on Linux. v0.1.7 and earlier are deprecated on npm.

Catalog version 0.2.8 -> 0.2.9.

* Repin community/handoff-od to v0.1.9

Addresses review round 6 on #6948: the default ZIP transport required an undeclared
host package. zipStrategies() had no candidate on a Linux box with only GNU tar, so
the default workflow failed at the deliverable with 'no working zip writer. Tried 0'.

v0.1.9 writes both formats in-process with node:zlib. engines: node >= 20 and zero
dependencies are now the whole requirement, and nothing external is invoked — which
also means a substituted archiver on PATH cannot affect the output.

Fixtures as requested: the default pipeline run with PATH pointing at an empty
directory, and a hostile tar/zip/bsdtar earlier on PATH asserted never to execute.
Output verified byte-identical against bsdtar, PowerShell Expand-Archive and Python
zipfile; a pax header now carries non-ASCII and long paths, which ustar mangled.

81 tests, zero skipped. v0.1.8 and earlier are deprecated on npm.

Catalog version 0.2.9 -> 0.2.10.
2026-08-18 11:08:54 +00:00
f1a0b60c6c chore: unify product name display to OpenDesign repo-wide (#6998)
* chore: unify product name display to OpenDesign repo-wide

Rename the user-facing product name from "Open Design" to "OpenDesign"
across documentation, web/desktop UI strings, i18n locales, prompts,
plugin/skill/design-system content, changelogs, and specs, matching the
landing-page rename already in flight.

Deliberately excluded so installed-app identity stays untouched:
- packages/release PRODUCT_NAME and everything derived from it
  (app bundle names, installer/registry identity, updater feeds)
- tools/pack, tools/release, apps/packaged, landing page (PR #6983)
- mocks/ recorded traces and LICENSE
- test fixtures and comments that encode real on-disk install paths
  (Open Design.app, Program Files\Open Design, AppData paths)

Validated with pnpm guard, pnpm typecheck, and targeted web/desktop/host
test runs over the renamed strings.

* fix(web): keep two-letter author monogram for CamelCase brand names

authorInitials split words on whitespace only, so the renamed single-token
author name "OpenDesign" produced a one-letter monogram. Treat lower-to-upper
CamelCase boundaries as word breaks so the avatar fallback stays "OD".

* fix(release): restore spaced Windows source artifact names in win.ps1

tools/pack still derives builder outputs from the unchanged PRODUCT_NAME
("Open Design"), so the asset script must look for the spaced filenames.
Matches the mac/mac-intel/linux asset scripts, which were already kept on
the original names.

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
2026-08-18 07:09:45 +00:00
CaprikaandGitHub e654592ba6 fix(daemon): point users at the Settings section that actually exists (#6831)
* fix(daemon): point users at the Settings section that actually exists

When image generation failed for a missing credential, the reply told
the user to configure it in "Settings → Media". No such nav item exists —
the section is "Media providers" ("媒体生成提供商" in zh-CN) — so users
following the instruction found nothing.

The cause is that Settings destinations were spelled independently
wherever a producer happened to need one: 29 provider errors in
apps/daemon/src/media/index.ts said a bare unnamed "Settings" (one said
"Settings -> Media Providers"), and the od-media-generation skill said
"Settings → Media". None of them were tied to the Settings dialog, so a
rename could not reach them.

packages/contracts/src/settings-nav.ts is now the single place a Settings
destination is spelled, and every producer reads it: the media provider
errors, the ElevenLabs voice-options prompt (daemon + contracts), and the
external-MCP auth guidance. e2e/tests/settings-nav-copy.test.ts holds the
constants against the labels apps/web actually renders and fails the
build if a producer re-inlines its own guess.

Swept the same drift class while here: both hatch-pet SKILL.md copies
pointed at "Settings → Pets", but #5517 folded the pet picker into
General, so the real path is "Settings → General → Pets".

Verified the gate red on the pre-fix tree (3 failing assertions) and
green after. Adjacent, not fixed: these strings are English and the
agent translates them ad hoc, so a localized client can still render a
section name that does not match its own UI — see the report.

Record: recvre8FrTE2Oa (V0.19.1 acceptance)

* fix(contracts): gate destinations on rendered nav, not the i18n dictionary

Review on #6831 caught that SETTINGS_EXTERNAL_MCP named a place users
cannot navigate to. `mcpClient` kept its Settings render block but lost
its sidebar nav item (SettingsDialog.tsx says so in its own comment), and
reconnecting an external MCP server now lives in the top-level
Integrations view. So the destination is not a Settings section at all.

The stale string predates this branch — it was hardcoded in
prompts/system.ts on main. But promoting it into a named constant made it
worse twice over: the docblock asserted "The Settings nav item where
external MCP servers are added/reconnected" in a canonical location, and
the new gate whitelisted it by construction, so the check built to catch
dead paths became the thing defending one.

The deeper defect was the gate's invariant. Comparing a constant to its
i18n key's value proves only that some dictionary entry matches — not
that anything renders it. `settings.externalMcpTitle` still resolves to
"External MCP" with no nav item behind it, which is exactly how the dead
path passed. The same hole covered SETTINGS_MEDIA_PROVIDERS.

Now the gate parses what the UI actually renders — `settings-nav-item`
buttons and INTEGRATION_TABS — and resolves those keys through the
dictionary, using it as a lookup rather than as the source of truth.
Verified by reintroducing "Settings → External MCP": the old gate passed
it, the new one fails with the rendered destinations listed.

Replaces SETTINGS_EXTERNAL_MCP with INTEGRATIONS_MCP_PATH
(`Integrations → MCP`) and adds a regression test pinning that a
dictionary-only label is rejected.
2026-08-13 08:42:27 +00:00
+13
lefarcenGitHubRay XiCHENGLONG WANGlefarcenPerishFireLooperJoey-nexuJoeybestthanaponopen-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>xiaoche-hubCheemsAmyMarc Chan_HDCoderbone3deep1962-collabNicholas-Xiongmehmet turacopen-design-release-bot[bot] <295937643+open-design-release-bot[bot]@users.noreply.github.com>open-design-botshangxinyu1elifive555555
356c8c364f feat(workspace-team): team workspaces, shared resources, workspace-scoped billing, and the #5517 redesign (#6142)
* fix(web): scope plugin URL imports to workspace

* test(web): cover billing interest remount lifecycle

* feat(packaged): enable the workspace-team transport for test-profile builds

The vela test backend now serves the workspace-team API, so a packaged build
baked with the test AMR profile should drive team projects / collab / resource
sharing there instead of leaving them dormant. Replace the single feature-test
equality check with a profile -> vela web origin map so adding a backend is one
entry, and keep every unlisted profile (prod above all) off.

* test(daemon): cover billing catch-up after SSE reconnect

* fix(collab): surface and start first materialization when opening a shared project

QA P0: a brand-new member on a fresh install joins someone's workspace, opens a
project from it, and sees no loading state and no files — content only appears
on a SECOND open, much later.

Two defects, both on the first `/collab/status` request that opening a shared
project makes.

1. No pull is started. The daemon's self-materialization block was gated on
   `callerIsOwner`, so a non-owner member got the placeholder record registered
   and nothing else. The web's auto-pull is gated on `publishedVersion`
   advancing past its cursor, and a fresh daemon's first status response cannot
   carry a published head: `collab.publishedVersion()` reads an in-process map
   that has never been written, and the real hub head is fetched
   fire-and-forget into `headEnrichmentCache` for a LATER poll to consume.
   Materialization therefore arrived only whenever a proactive lane (hub push /
   reconnect catch-up / the 30s recovery floor) next fired — the "second open"
   the report describes.

2. No download state is shown. `useProjectCollab.downloadPending` — which picks
   DesignFilesPanel's "Syncing files from the team…" over its empty state and
   create-a-file CTAs — reads only `publishedVersion > cursor`,
   `contentTransferState`, and an in-flight pull. All three are blank on that
   first response, so the member was shown an empty project inviting them to
   start creating over content that was still on its way.

The fix makes the placeholder stamp the load-bearing signal on both sides:

- `materializePlaceholderOnOpen` (routes/collab-sync.ts) replaces the
  owner-only block: any viewer opening a shared project whose only local record
  is an unmaterialized placeholder starts its pull on that same request,
  through the same coalesced flow. The retracted-share heal in its catch stays
  owner-only — a member who hits the hub tombstone has merely lost access and
  must not unshare anyone's project.
- `/collab/status` reports `awaitingFirstMaterialization`, a purely local,
  synchronous fact that needs no hub round-trip and is therefore available on
  the very first response. `od collab status` prints it too.
- `localFilesAreNotTheContentYet` (useProjectCollab.ts) makes it always
  download-pending, bypassing the member-only `shouldAutoPull` gate: whether the
  local files are the content is a fact about the record, not about who may
  pull it, so the owner of an unmaterialized placeholder (the reinstall case,
  recvqzaDvUU6B3) stops seeing the same empty state too.

Red evidence: all six new specs fail on origin/feat/workspace-team @ 793d7b92c
— the first status response reported `awaitingFirstMaterialization: undefined`
with publishedVersion/materializedVersion/contentTransferState all null,
`beginContentTransfer` was never called for a member's first open, and
`downloadPending` was false for both the member and the owner.

* chore(packaged): inject the vela web origin at build time, not source

This repository is public. Commit 319cb42b3 added a profile -> vela web
origin table to apps/packaged/src/sidecars.ts, which put an internal
environment's hostname into product source and would have published it on
merge. The same hostname had also reached apps/web/src/runtime/amr-guidance.ts
as a new profile row.

Internal AMR environments are not public (one of them fronts a Stripe
sandbox), so their origins now travel the same build-time path POSTHOG_KEY
already uses:

  release-beta.yml env (per-profile secret)
    -> tools-pack config (OD_VELA_WEB_URL)
    -> open-design-config.json (velaWebUrl)
    -> packaged config -> daemon spawn env (OD_VELA_WEB_URL)
    -> GET /api/integrations/vela/status (consoleOrigin)
    -> web runtime console links

The workspace-team gate keeps its meaning and gets stricter: it now needs an
allowlisted AMR profile AND an injected origin. `prod` can never satisfy the
profile half, so a stable build stays dormant however it is configured, and a
feature-test/test build whose secret is unset stays dormant too rather than
pointing the transports at an unknown backend.

A build with no secret configured therefore ships with workspace-team silently
off — the same failure mode as a fork build without POSTHOG_KEY, and the safe
direction for an unreleased feature.

apps/packaged/tests/source-origins.test.ts is a standing guard: every absolute
URL literal under apps/packaged/src must resolve to a publishable host, so the
next backend origin cannot be hardcoded there either.

Secrets required for a workspace-team dogfood build:
VELA_WEB_URL_FEATURE_TEST, VELA_WEB_URL_TEST.

* test(e2e): follow the #5517 plugin-details route in the visual capture

`captures the home plugin use staged surface` has been failing on this
branch since before the main merge. It was written against the pre-#5517
UI: clicking a marketplace card opened a role="dialog" details overlay
with a per-slug `plugin-details-use-<id>` action.

#5517 made plugin details a full-page route — `openCardDetail` calls
navigate({ kind: 'marketplace-detail' }) for plugin records, rendering
`PluginDetailView` at /marketplace/<id> whose Use control is a single
`plugin-detail-use` button. Per the product rule that the accepted
feature-branch UI is the source of truth, the test follows the route
instead of the UI being changed back.

Asserting the route alone was not enough: the detail surface refetches
the record from /api/plugins/<id>, which the visual fixture never mocked
(it mocks the list, */preview and */apply), so the route rendered its
"Failed to load plugin: HTTP 404" branch. configureVisualPage now serves
the single-plugin GET from the same VISUAL_PLUGINS fixture.

The capture asserts the Use button rather than the `plugin-detail` shell
because the shell also renders for the loading and load-failed states.

* fix(packaged): restore masked CLI icons (#6154)

* Sync local workspace-team batch: UI polish, popover simplification, update-reminder progress (#6156)

* UI polish batch: import-dialog cleanup, settings toast placement, tab chrome fixes

- Plugin/skill import dialog: drop per-card icons, left-align card content
- Settings page: center the autosave pill under the top nav
- Avatar model menu: soften the pinned footer divider
- Workspace tabs: pinned entry tab always reads as Home; remove tab-search
  button and its popover
- Model picker: wider row gap in the two-pane browse popover
- Community header: unpin (scrolls with content)
- New update-reminder dialog assets and strings
- Keep entry-settings-button as the signed-out settings entry (e2e contract);
  align NextStepActions with the navPlugins key rename

* Update reminder: simulate in-place download progress instead of linking out

The strip's confirm now runs a staged progress state ('updating') with an
eased fake download bar, then marks the version updated — replacing the
jump to the GitHub releases page.

* test(web): retire AvatarMenu account-row suites after the popover simplification

The composer popover was reduced to a model picker (2026-07-24): the Open
Design account row — plan badge, balance, wallet fallback, upgrade/console
links — no longer renders there, so the nine suites asserting that surface
(including the workspace-balance and billing-permission gates that landed
upstream meanwhile) can no longer pass. Drop them and recast the remaining
account-row test as the guard for the new invariant: a signed-in AMR status
must render no account UI in the popover.

Also drop the stale onClose prop from the new upstream sign-out-confirm
test; the rail no longer takes one.

* Design-systems toast: anchor to the pane's top edge, pill radius

* Update reminder strip: progress ring on the button, percent face

While updating, the collapsed strip no longer stacks a label + bar above
the rocket button. Progress draws as a brand-green ring hugging the round
button's edge (5px band reserved so the button never shifts), and the
button face swaps the rocket for the bare percent — 'N%' while
downloading, 完成 (updater.done) at 100. The full 正在下载更新 N% label
stays reachable via the hover bubble and the ring's aria-label. Demo
payload version bumps to 1.4.6 so the once-per-version card re-arms.

* fix(web): restore design kit section spacing (#6155)

* fix(web): carry the plugin back to Home when Use runs from the detail route

Product call: using a plugin from the full-page detail route should land on
Home with that plugin already selected and its brief pre-filled, exactly
like the marketplace card's "Try it".

It did not. `PluginDetailView.onUse` applied the plugin, kept the result in
local state, and navigated home. `App` renders that route outside
`EntryShell`, so the navigation unmounted the holder and dropped both the
plugin and the brief — Home came up empty. The "redirected to Home with the
brief pre-filled" note under the button could never be seen either, since
the navigation fired immediately; it is removed rather than left lying.

Reuses the existing channel instead of adding a parallel one: onUse now
publishes the same `createPluginUseHandoff` payload "Try it" produces, and
Home's existing pendingPluginUseHandoff path applies it unchanged. The
handoff is parked in module scope — not on `window` — so it survives the
unmount, and `EntryShell` claims it in a lazy state initializer so it
arrives in the same commit as the mount. Reads are destructive, so a
handoff applies once and does not re-fire on the next visit to Home.

Red first: PluginDetailView.use-handoff.test.tsx fails on the unfixed code
at the handoff assertion (navigation and the failure path already passed),
and goes green with this change. The e2e visual capture that asserts
home-hero-active-plugin should follow without weakening its assertion.

test(e2e): match the settings surface in both #5517 presentations

openSettingsDetailsFromHeader still waited on `.modal-settings[role="dialog"]`.
#5517 routes the entry's settings to /settings, where SettingsDialog renders
in presentation="page" mode with `role="region"`, so that selector never
matched the page presentation: an already-open settings surface looked
absent and the helper fell through to hunting for entry triggers that
surface does not carry, failing 16 captures on one line.

Reuses `settingsSurface` from amr.ts — the existing bare `.modal-settings`
matcher both presentations share — rather than adding a third local
variant. That helper also documents why the `role="dialog"` fallback was
actively wrong: AvatarMenu is a dialog too, so it could resolve to the
account menu.

* test(e2e): open the nav rail before reaching for entry settings

The bare-`.modal-settings` change alone did not move the settings-workspace
lane: all 16 captures still died on `.settings-icon-btn`, because the
failure happens before the surface selector matters. None of the four
triggers was ever visible.

amr.ts already documents why. #5517 moved the entry settings chip into the
nav rail footer, and a collapsed rail is `inert` + `aria-hidden`: the chip
is present but invisible to `getByRole`, and even a programmatic
`element.click()` is a no-op. `openSettingsDialog` expands the rail first
(`ensureEntryRailOpenIfPresent`), clears loading, dismisses the privacy
dialog, matches the surface with the shared bare class, and ends its
trigger chain on the settings aria-label instead of `.settings-icon-btn`,
which the entry surface does not carry. It also clicks
`entry-settings-open-details`, so it is a superset of the local copy.

So `openSettingsDetailsFromHeader` now delegates to it and keeps only its
name, leaving exactly one settings opener instead of a third variant.

* test: realign the suites #6156 outpaced, and reach Settings from a project

#6156 (4e3161751) landed a UI batch whose test fallout was only partly
handled. Four Vitest files and four e2e call sites still describe the surface
as it looked before it, and the Playwright settings opener never had a trigger
that exists once a project is open.

Vitest — the assertions now read the surface that shipped:

  - community-view: the type tabs traded their `<small>` count badges for type
    icons, so `readFacets().badge` was `NaN`. Counts move to a new
    `readFacetCardCounts()` that drives each tab and counts the grid — the same
    array the badges were derived from, so the catalogue-fidelity guard
    (including "never grids more than the whole catalogue") survives the
    badge's removal rather than being dropped.
  - home-logo-assets: the rail's signed-out brand header is gone (with no cloud
    identity the rail starts at the search box), so it can no longer carry
    `od-brand-glyph`. Keeps the guard that still means something: it must never
    fall back to the retired raster app icon.
  - acceptance-visual-fixes: recvq4iEq1Esno's fix is intact — page mode still
    hides only `.settings-chrome-btn`, leaving the autosave pill visible. #6156
    re-added the bare `.settings-page-shell .settings-chrome` selector purely to
    centre that pill under the top nav, so assert the invariant the row is
    about (no page-mode `display: none`) instead of the selector's absence.
  - FileViewer: the new `artifact-preview-first-load` cover is a `role="status"`
    of its own, so `findByRole('status')` had become ambiguous. Scope the
    success assertion to the toast.

e2e — `entry-nav-settings` no longer exists anywhere in `apps/web/src`; the
rail item carries `entry-settings-button`. #6156 renamed it and updated
entry-chrome-flows' two `toHaveCount(0)` assertions but not the four places
that click or await it (critical-smoke, entry-topbar x2, entry-chrome-flows x2).

`openSettingsDialog` also only ever knew entry-shell triggers, all of which
live on Home. From a project every one of them is absent — and
`EntrySettingsMenu` and `AppChromeHeader`'s `SettingsIconButton` are both
unrendered — so the surface never opened and the failure surfaced as a missing
`.modal-settings`. Add the project surface's real entry: the composer model
popover's pinned `avatar-open-execution-settings`, with the topbar switcher's
`inline-model-switcher-open-settings` as a second route.

Not addressed here, deliberately: #6156 also deleted the `settings-general-field`
block that rendered `AppearanceSection`, orphaning the component. That removes
the product's only System/Light/Dark control (the rail's account menu dropped its
theme row *because* Settings owned it), so the 11 `SettingsDialog.execution`
appearance failures are reporting a capability loss, not a stale expectation.
Restoring it is a product call, so those tests are left red rather than weakened.

* test(e2e): finish the popover the settings chain opens on a project surface

`openSettingsDialog`'s trigger chain ends on OPEN_SETTINGS_LABEL, which matches
`Account & settings` — and that is `avatar.title`, the label on AvatarMenu's
own trigger. So on a project surface the chain does find something and clicks
it, but what opens is the composer's model popover, not Settings. The
follow-through step then looked only for `entry-settings-open-details`, which
#5517 left unrendered, so the run gave up with a missing `.modal-settings`
after three identical attempts.

Widen the follow-through to the two rows that actually route to Settings from
a project — AvatarMenu's pinned `avatar-open-execution-settings` and the topbar
switcher's `inline-model-switcher-open-settings` — so whichever popover the
first click opened gets finished.

* test(e2e): pin the AMR selection assertion to the agent card

With Settings actually opening from a project surface again, the surface-wide
`/Open Design/i` in amr-logout-requires-relogin resolves to the sidebar's
"Open Design MCP" `settings-nav-item` — which carries no `aria-pressed` — not
to the AMR agent card's select button. The looseness was invisible while the
surface never opened.

Read the toggle through `settings-agent-card-amr` instead, so the assertion
names the thing the test is about: AMR stays the selected agent.

* test(e2e): prove Settings opened via its section nav, not a heading probe

`prepareVisualSettingsDialog` gated on a heading matching
/Settings|General|Execution mode/. Now that the surface opens from a project it
lands on the execution section, whose heading reads "Models & providers" — and
the surface's own <h2> is consumed as its accessible name via aria-labelledby
anyway, so the probe could not match in either presentation.

Assert `settings-nav-execution` instead, which is what critical-smoke already
uses for the same purpose and holds for both the modal and the routed page.

* test(web): retire the theme/appearance cases the product cut

Product decision (2026-07-28), verbatim: 「主题设置不要了,因为 workspace 功能不
支持暗色主题,要干掉,并且之前用户如果设置了暗色主体,需要强制改成明亮色主题」

So #6156 deleting the `AppearanceSection` render site was the right direction,
and the NON-ALIGNMENT #9 note that argued for keeping the segmented control as
the product's last "follow system" entry point is formally overturned. These
cases are retired because the capability is gone by decision — NOT to turn CI
green. The source-side removal (orphaned `AppearanceSection`, onboarding
light/dark toggle, `settings.appearance*` keys, `trackSettingsAppearanceClick`,
plus forcing stored dark/system back to light for existing users) lands
separately on `fix/remove-theme-force-light`.

Deleted (8) — their whole subject was theme or the accent it carried:
  - offers the theme segmented control, and System leaves the document theme unset
  - applies the stored default accent color even though the picker is gone
  - writes the picked theme to the document and autosaves it
  - live previews the configured theme on open, and System leaves no explicit
    document theme
  - reverts an unsaved appearance preview back to the saved appearance when the
    dialog closes
  - persists System mode explicitly and preserves accent variables without an
    explicit document theme
  - keeps a stored non-default accent applied and carries it through an autosave
  - localizes the theme controls in Chinese

Nothing was left behind as a loose "still renders something" assertion, and no
live coverage went with them: the close-button/`onClose` path the revert case
also touched is already covered at five other call sites in this file.

Kept (3) — theme was only the vehicle, the subject is still shipping. Each now
rides the notifications completion-sound toggle, seeded on so the single click
is a real state change (the pills no-op when clicked in their current state):
  - reconciles the open settings draft when the parent agent CLI env changes —
    the only integration proof that `reconcileAmrModelChoice` /
    `reconcileAmrProfileEnv` actually run through the real draft on autosave;
    its two siblings are pure unit tests and do not cover that path.
  - drops a pending autosave when explicit onboarding reset unmounts Settings —
    subject is the pending-autosave drop and the draft the reset carries.
  - still autosaves an unrelated edit that lands during a silent-update save —
    subject is autosave bookkeeping (success must only advance
    autosaveLastSavedRef for allowSilentUpdates).

The enclosing block is renamed 'SettingsDialog draft reconciliation' since it no
longer describes appearance, and its document theme/accent teardown is dropped —
nothing in it writes those any more. 144/144 green.

* test(e2e): realign the visual workspace captures with the narrowed surfaces

Three deliberate narrowings shipped without this Playwright lane following, so
eight captures were driving UI that no longer renders. All eight are stale
fixtures; none was a regression.

  - `ef9c8cd8b` made the home top-bar `InlineModelSwitcher` `compact`, and
    `EntryShell` (its only call site) always passes the flag. In compact mode the
    popover is just the active agent's model radio list plus the route to
    設定 → 執行; the mode segmented control, agent grid, account block, and both
    searchable dropdowns live in the non-compact branches. `68cecac1c` records
    that product confirmed this narrower shape, so acceptance #40 ("home 页不能
    切换 cli 了") is working as designed.
  - `4e3161751` (#6156) removed the Open Design account row from `AvatarMenu`
    (plan badge, balance, wallet fallback, upgrade/console links) and retired the
    nine equivalent Vitest suites — but not these.
  - `56b538aa4` moved the design-system picker out of the staged-context bar, so
    `staged-contexts` is no longer unconditionally mounted.

Where a surface still ships, the capture follows it and the assertion got
sharper rather than looser: staged contexts now stages a real attachment and
names the chip; the topbar/avatar captures assert the surviving model picker
plus the *inverse* invariant (no account block, no upgrade button, no balance),
gated on a counted `vela/status` request so a negative cannot pass by racing the
fetch. `:57` additionally pins `inline-model-switcher-mode-daemon` at count 0, so
re-mounting the CLI console on Home fails here again.

`:167` (topbar BYOK model dropdown) is deleted: in compact mode it would
re-capture the identical popover as `:141`, which is a duplicate screenshot
rather than coverage. `:141` absorbed its guard.

`:244` (avatar reasoning readout) turned out never to have passed — not a #6156
casualty. `VISUAL_CLI_AGENTS`' codex entry declares `models` only, while
`AvatarMenu` draws the readout solely for agents reporting `reasoningOptions`, so
it has been red since `68cecac1c` authored it. Fixed by declaring
`reasoningOptions` on a test-local codex fixture, matching what the real daemon
reports (`apps/daemon/src/runtimes/defs/codex.ts:164`).

Nothing was dropped without a verified live home elsewhere in the same CI lane
or in Vitest: the AMR plan/balance/upgrade card at `visual-settings.test.ts:30`,
the searchable dropdowns at `visual-settings.test.ts:93`/`:156`, the upgrade
attribution URLs in `InlineModelSwitcher.test.tsx:585` and
`AvatarMenu.test.tsx:283`, the "no account row" invariant at
`AvatarMenu.test.tsx:528`, and real staged-context chips in
`project-management-flows.test.ts`. The one assertion with no surviving home is
the Open-Design-first agent ordering, whose grid no longer renders anywhere;
that is called out in the test comment rather than silently lost.

* test(e2e): give the AMR upgrade capture a billing-capable workspace

`settings-agent-card-amr-upgrade` stopped rendering for this capture because
`amrCardCanUpgrade` (SettingsDialog.tsx:4651) now also requires
`workspaceContext?.permissions?.canManageBilling` — this branch's
workspace-scoped billing work, i.e. the feature #6142 exists to ship. The gate
is correct; the fixture was not representing anyone entitled to pass it, since
`mockSignedInVelaAccount` establishes a Vela session but no workspace context.

Stub `/api/workspace/context` with a personal-owner context inline in this one
test rather than widening `mockSignedInVelaAccount`, which visual-workspace
shares. The assertion is unchanged — the upgrade entry is a real capability for
billing-capable members, so the fixture represents one instead of the assertion
being relaxed.

Verified the stub does not quietly satisfy the three assertions that already
passed: `planId: null` keeps `resolvePlanTier` falling through to `plus`, and
billing still 404s so the balance stays `$247.51`.

Expect one legitimate baseline diff on `visual-settings-open-design-account`:
the rail now renders signed-in workspace chrome, which is the only state in
which the upgrade entry exists at all.

* test(e2e): leave projects through shipping chrome, and reach /projects reliably

Two dead locators and one under-budgeted wait, all test-side.

`openNewProjectModal` (rail.ts) probed `entry-nav-new-project` then
`entry-nav-projects`, both deleted from EntryNavRail by the #5517 redesign
(`b55f17169`, `f16075f7e`) — `onNewProject` is still destructured there but has
zero call sites. Its body is byte-identical to main, where both testids exist, so
main takes the rail path and never reaches the fallback this branch always hits.
The live affordance is `designs-new-project` / `designs-empty-new-project` in
DesignsTab, inside `entry-view-projects`; `/projects` has no UI entry at all
(entry-chrome-flows:373-382 already documents that and routes directly).

The fallback then failed for a second reason: `entry-view-projects` does ship
(EntryShell.tsx:1579) and `/projects` does route to it, but it was asserted on
the 10s default budget right after `waitUntil: 'domcontentloaded'`. `apps/web`
mounts `src/App` via `dynamic(..., { ssr: false })`, so domcontentloaded fires
while only the boot shell exists — every suite that does this correctly waits out
`Loading Open Design…` with `T.long` first. Now routed via
`history.pushState` + `popstate` (the client router listens) instead of a full
reload, boot shell waited out, and both the view and the create button given
`T.long`.

`ensureRailOpen` was also called unconditionally, but it ends in a hard
`expect(workspace-home-rail-toggle).toBeVisible()` and that testid only renders
for `isPinned && active` (WorkspaceTabsBar.tsx:1474-1510) — from inside a project
it does not exist, so the helper hard-failed on a control this flow never needed.
Now best-effort and gated on `.entry`, kept only so the
`visual-new-project-modal` baseline does not churn.

`real-daemon-run.test.ts:547` hung on `/back to projects/i`, which matches
nothing on a project surface: `AppChromeHeader` owns that aria-label but is no
longer mounted anywhere, and ChatPane's top-left slot resolves
`onCollapse ?? onBack` while ProjectView passes both, so it is always
`chat-collapse-toggle` (deliberate, `884ed1085`). Replaced with a local
`leaveProjectForEntry` that clicks the pinned entry tab — the shipping way out —
so the isolation journey still leaves through real chrome instead of a URL jump.

Dropped `getActionablePoint` with its only consumer. Removing the reload from
~25 call sites across 15 suites was audited against the stub-order-sensitive ones
(settings-media-providers, new-project-ds-picker-*, api-empty-response,
visual-entry); all install routes before their first navigation.

Correcting my own earlier misattribution: `.ws-tab` / `.ws-tab-label` are
FileWorkspace's in-project file strip and are fully intact, `role="tab"` and
file-name labels included. #6156 rewrote WorkspaceTabsBar, which renders
`.workspace-tab` / `.workspace-tab__label` — a different component. Upload→tab is
also intact (`FileWorkspace.uploadFiles` still calls `openFile`). Five of those
six failures merely routed through the broken helper above; the sixth was the
dead back-button. No `.ws-tab` assertion was rewritten.

* fix manual edit across workspace pages (#6160)

* fix(collab): keep process-local presence alive when the collab transport is off (#6170)

`createVelaCliCollabClientFromEnv` returns `null` unless the run opted into
the vela-cli collab transport, but `server.ts` wired the presence routes with
an object literal of arrow functions closing over that client. A literal is
unconditionally truthy, so `registerCollabPresenceRoutes` always saw a `cloud`
dependency: its `deps.cloud ?? null` could never resolve to `null`, the
process-local `presence.present()` fallback became dead code, and every
presence request that reached the cloud branch dereferenced `null` and was
answered `502 collab_presence_unavailable`.

`POST /api/projects/:id/presence/leave` carries no shared-project
precondition, so it failed on any project id in any run without the
transport — every stable/prod packaged build (the packaged workspace-team env
is gated to the `feature-test`/`test` AMR profiles) and every plain
`tools-dev` run. `server.ts` is `@ts-nocheck`, so the null dereference was
invisible to `pnpm typecheck`.

Route the dependency through `createCollabPresenceCloudClient`, which lives in
the checked route module and states the invariant: a `cloud` dependency exists
if and only if a transport exists. Removing its guard is now a typecheck
error, so the bug class cannot come back at the construction site even while
`server.ts` stays unchecked.

* fix(web): scope desktop vibrancy to macOS (#6171)

* fix(web): remove the theme setting and force every install back to light (#6168)

Product removed theme selection: the workspace surfaces shipped for team
workspaces have no dark tokens, so dark mode renders a broken app.

Deleting the picker is not sufficient on its own. Every install that ever
opened it still has `theme: 'dark'` — or `'system'`, which resolves dark on a
dark OS — persisted in localStorage, and a stored value does not move when the
default does. So the theme is now coerced on READ, at all three points a
persisted value can reach the document:

- `loadConfig()` funnels `parsed.theme` through `resolveAppTheme()` and marks
  the config migrated so the coerced value is written back once.
- `applyAppearanceToDocument()` stamps `data-theme="light"` unconditionally.
  The attribute must be PRESENT, not merely non-dark: every dark CSS rule is
  gated on `html:not([data-theme])` / `html:not([data-theme="light"])`, and
  every JS theme reader (shiki, ConnectorLogo, SketchEditor, TerminalViewer,
  connectorBrandColor, MentionNode) falls back to `prefers-color-scheme` only
  when the attribute is absent.
- The pre-hydration inline script in `app/layout.tsx` stamps light before React
  mounts, outside its try/catch so a throwing storage read still leaves the
  attribute set.

Electron's `themeSource` defaults to `system`, which colours everything the web
layer does not own (macOS vibrancy glass, native menus/dialogs) on a dark-mode
Mac — including the splash, before the renderer's appearance IPC lands. It is
now pinned to light before the first window exists.

Removed surfaces:

- `AppearanceSection` in SettingsDialog, orphaned by #6156 (zero call sites),
  together with its `settings-general-block--appearance` styles. Its docblock
  claimed the control was "deliberately kept … NON-ALIGNMENT #9"; that decision
  is superseded, so the comment goes with the code.
- The onboarding welcome page's sun/moon toggle — the last reachable theme
  writer — and the `onThemeChange` prop chain behind it
  (App → EntryView/ProjectView → EntryShell → OnboardingView).
- The orphaned theme row in EntrySettingsMenu and its styles.
- i18n keys `settings.appearance`, `settings.appearanceHint`,
  `settings.themeSystem`, `settings.themeLight`, `settings.themeDark` across
  `types.ts` and all 19 locales.
- Analytics `trackSettingsAppearanceClick`, `SettingsAppearanceClickProps`, and
  the `settings_popover` `appearance` element.

`'appearance'` survives only as a legacy settings deep-link token that
`normalizeSettingsSection` folds into General, so an old link is not a type
error.

Red-first: tests/state/force-light-theme.test.ts and
tests/components/theme-settings-removed.test.tsx were written against the
unmodified branch and went red (11 failing) before any source change.

* fix(web): make the update surfaces read real data (#6163)

* fix(web): make the update surfaces read real data

#6156 (4e3161751) shipped two update surfaces whose every field was
invented. `EntryShell` held a private `updateReminderStage` machine keyed on
a literal `'1.4.6'` (the app ships 0.16.1), the rocket's confirm ran a 200ms
`setInterval` easing a fake bar toward 100% and then wrote "updated" to
localStorage, and the cover dialog listed three hardcoded English notes past
i18n over a committed 432 KB JPEG. Nothing in that path made a single
network, IPC or daemon call — a user who clicked "立即更新" watched a
progress ring finish and got no update.

Both surfaces now sit on the real machinery that already existed:

The cover dialog becomes the post-update highlights surface and REPLACES the
bottom-right `WhatsNewPopup` card rather than coexisting with it. Cover art,
release headline, bullets and link all come from the hosted highlights
document via `/api/whats-new`; the title states the running version from
`useAppVersion()` (`/api/version`). It keeps that card's show gate verbatim —
once per highlight `id`, only with content, only on the home surface after
the app returns on a new version — and its existing analytics
(`whats_new_popup` surface-view / click). Its footer is close + open-release;
there is no "cancel / update now", because reporting what already shipped is
all this surface does. Bullets come from a new `whatsNewNotesFromBody`, which
splits the document's `body` per line and strips operator list markers.

The rocket keeps its look and becomes the real updater's ready indicator in
the bottom-left rail footer, replacing that indicator's arrow glyph. It is
gated on the updater's own `shouldShowControl` (installer downloaded and
never opened, desktop only) and still drives `openUpdaterInstaller` →
`quitAfterUpdaterInstallerOpen` through the untouched panel, watchdog and
restart-safety preflight. The progress ring and percent face are gone on
purpose: the indicator only exists once the download has finished, so any
percentage it drew would be invented.

Deleted: `lib/update-reminder.ts`, `UpdateReminderDialog.tsx` and its module
CSS, `public/update-reminder-cover.jpg`, the now-dead
`entry-updater-menu__button/__glyph/__progress` rules, and the unused
`accountNotice` rail slot. The three `updateReminder.*` i18n keys and the
orphaned `whatsNew.dismissAria` are replaced by one `whatsNew.updatedTitle`
across all 19 locales.

A source-level guard (`tests/update-surface-real-data.test.ts`) pins the
invariant that outlives the deletion: no quoted `x.y.z` and no `setInterval`
in the update surfaces, and the placeholder module and cover art stay gone.

* fix(web): name the release-notes destination on the what's-new CTA

Product review of the five open calls closed all five; only the CTA copy
changes. The button opens the release notes, so it now says so —
「查看更新说明」 in Chinese (product's wording, verbatim), with the other 18
locales aligned on "view the release notes" instead of inheriting the old
"see what's new" tease. `whatsNew.cta` had exactly one consumer left (the
dialog) once it replaced the bottom-right card, so the key is repurposed in
place rather than forked.

The other four calls were approved as implemented, and two of them overturn
conventions the old comments still asserted, so those comments are synced:

  - `lib/whats-new.ts` documented the toast's deliberate Escape exemption
    (Escape hid the card WITHOUT spending the highlight). As a focus-trapping
    modal that exemption is retired — every close path marks seen — and the
    docblock now says which paths and why.
  - `contracts/api/whats-new.ts` described `title` as generic copy, `imageUrl`
    as an image "beside the copy", `body` as prose, and `linkUrl` as the "See
    what's new" link. The dialog reads them as headline / top-inset cover /
    one-bullet-per-line / release-notes link, so each field now documents the
    role it actually plays. Comment-only; no shape change.

* fix(web): never state the placeholder version on the what's-new dialog

Review catch (nettee, #6163): `useAppVersion()` reads /api/version at runtime,
so it necessarily boots on `APP_VERSION_PLACEHOLDER` and resolves a round-trip
later. This dialog rendered as soon as /api/whats-new resolved and printed the
hook unconditionally — so a highlights fetch that won that race painted
"Open Design 0.0.0 is here" on first frame. An invented version string, on the
one surface this PR exists to make truthful. The new suite could not see it
because it mocked the hook to an already-resolved value.

Fixed by falling back rather than gating the whole render: gating would delay
the dialog behind a second round-trip on slow networks, while the highlights
document already carries the running version in its `version` field — the
daemon stamps it for display (see contracts/api/whats-new.ts), which is what
fed the old bottom-right card's eyebrow. So `statedAppVersion()` prefers the
resolved hook, falls back to the document, and returns null when neither can
name one; on null the dialog waits, because highlights are worth nothing under
a headline that lies. The same derived value feeds the title AND every
`app_version` prop, so analytics cannot ship the placeholder either.

`APP_VERSION_PLACEHOLDER` moves out of ./provider into a new leaf
analytics/app-version.ts alongside an `isResolvedAppVersion()` predicate, so a
surface that must not PRINT a placeholder can test for it without importing the
analytics client. The provider keeps its own use of the constant.

Regression tests, red before this commit (`.red-evidence-version-race.txt`
reproduces nettee's exact string, `expected 'Open Design 0.0.0 is here…' not to
contain '0.0.0'`): the placeholder never paints while /api/version is in
flight, it never reaches the surface-view analytics, the title switches to the
running version once the hook resolves, and a document with no usable version
waits instead of inventing one. The suite's provider mock is now a mutable
holder so both sides of the race are reachable.

Also caught by this PR's own source guard: quoting the literal in a docblock
tripped it, so the comment names the constant instead.

* test(e2e): unmask the AMR runtime lane and repair the visual settings/workspace captures (#6174)

Three fixture defects, each verified by running the lane rather than reading it.

- amr-run-failure-recovery.test.ts was still `mode: 'serial'`. A second
  `describe.configure({ timeout })` cannot clear it — configure only
  overwrites the keys it is given — so one failure skipped the eight cases
  behind it and the lane reported them as "did not run". e2e/AGENTS.md
  forbids serial groups outright.
- configureVisualPage's `**/api/**` catch-all (added by #6126, which is on
  main) answered `/conversations` with 404. ProjectView mounts ChatPane only
  once a conversation resolves, and both listConversations and
  createConversation swallow a non-ok response, so the project opened with no
  conversation and no error, ChatPane never mounted, and every capture that
  enters the workspace died on `chat-composer` not existing. Supplies the
  conversation boundary the catch-all closed without replacing.
- The BYOK tab is named "API providers" since #5971 renamed
  `settings.modeApiMeta`; `getByRole('tab', { name: 'BYOK' })` matched
  nothing and three captures hung until the test timed out. The topbar BYOK
  capture also left `[MOCK_AGENT]` installed, contradicting its own "a BYOK
  config has no local agent" premise: an installed agent wins the popover and
  renders its model radiogroup instead of the BYOK rows.

Co-authored-by: lefarcen <ontf116@gmail.com>

* Home shell productization batch: message center, What's-new on release data, update-reminder & updater popup restyle (#6162)

* UI polish batch: import-dialog cleanup, settings toast placement, tab chrome fixes

- Plugin/skill import dialog: drop per-card icons, left-align card content
- Settings page: center the autosave pill under the top nav
- Avatar model menu: soften the pinned footer divider
- Workspace tabs: pinned entry tab always reads as Home; remove tab-search
  button and its popover
- Model picker: wider row gap in the two-pane browse popover
- Community header: unpin (scrolls with content)
- New update-reminder dialog assets and strings
- Keep entry-settings-button as the signed-out settings entry (e2e contract);
  align NextStepActions with the navPlugins key rename

* Update reminder: simulate in-place download progress instead of linking out

The strip's confirm now runs a staged progress state ('updating') with an
eased fake download bar, then marks the version updated — replacing the
jump to the GitHub releases page.

* test(web): retire AvatarMenu account-row suites after the popover simplification

The composer popover was reduced to a model picker (2026-07-24): the Open
Design account row — plan badge, balance, wallet fallback, upgrade/console
links — no longer renders there, so the nine suites asserting that surface
(including the workspace-balance and billing-permission gates that landed
upstream meanwhile) can no longer pass. Drop them and recast the remaining
account-row test as the guard for the new invariant: a signed-in AMR status
must render no account UI in the popover.

Also drop the stale onClose prop from the new upstream sign-out-confirm
test; the rail no longer takes one.

* Design-systems toast: anchor to the pane's top edge, pill radius

* Update reminder strip: progress ring on the button, percent face

While updating, the collapsed strip no longer stacks a label + bar above
the rocket button. Progress draws as a brand-green ring hugging the round
button's edge (5px band reserved so the button never shifts), and the
button face swaps the rocket for the bare percent — 'N%' while
downloading, 完成 (updater.done) at 100. The full 正在下载更新 N% label
stays reachable via the hover bubble and the ring's aria-label. Demo
payload version bumps to 1.4.6 so the once-per-version card re-arms.

* Home shell productization batch: message center in the account menu, What's-new on real release data

- Message center: entry moves into the account menu under 设置 (bell row);
  MessageCenter becomes controllable (hideTrigger + open/onOpenChange +
  onUnreadCountChange) and mounts persistently in EntryNavRail so unread
  polling survives the hover menu; unread shows as a red dot on the account
  avatar (and on the menu row); avatars go from squircle clip to 8px rounded
  squares; panel filter segmented control and mark-all-read go pill-radius.
- What's-new: the post-update highlights card now wears the shared
  release-dialog shell (cover / title / dotted notes / pill footer) while
  keeping its REAL /api/whats-new data source, show-once-per-id timing,
  soft-hide Escape/backdrop semantics, and analytics. The demo-payload
  update-reminder dialog and its simulated download no longer trigger; the
  stage machine and progress strip stay for the real updater-feed wiring.
- Rail chrome: brand logo and the in-rail collapse control are gone — the
  rail starts at the search box and folding lives in the pinned Home tab's
  toggle; the signed-out cloud tip bottom inset now matches its sides.
- Brand picker (modal variant): side-nav split — search + vertical category
  nav left, a roomy 2-up borderless gallery right, content flush to the
  column line with hover fill/ring bleeding past it; logo tiles lose their
  boxed edge; selected template chip and the composer mode pill share the
  same selected ring, with the mode glyph optically matched at 13px.
- Tab launcher: rows are icon + label only (descriptions dropped) with a
  rotating four-hue icon palette; the 新建空白页面 entry is paused behind
  ENABLE_BLANK_PAGE_WORKSPACE_ENTRYPOINT and its PageCreator suites skipIf
  on the same switch.
- Move-to-team confirmations: extracted MoveToTeamConfirmDialog, no backdrop
  blur (dim only), and the confirm keeps its brand-green label through the
  mention-home ink-pill restyle in every interaction state.
- Publish CTA: hover inverts to ink fill with brand-green icon + label.
- Plus assorted in-progress workspace polish (composer, viewer, settings,
  model icons) riding the same branch iteration.

* Restyle the updater ready popup to the update-reminder dialog language

Cover art on top (compressed 1296x555 jpg, natural aspect), the orange
accent icon tile removed, secondary action as a soft white pill and the
install CTA as a black pill with brand-green ink, and a stacked footer so
the silent-update checkbox keeps a full-width single line instead of
wrapping beside the widened pills.

* Signed-out rail: message-center entry under 设置

The signed-out rail has no account menu (where the 消息中心 row lives when
signed in), which left the message panel with no opener at all. The rail
item carries the unread dot.

* Composer row: pin chip glyphs against flex shrink

The row-wide min-width: 0 that lets chip labels ellipsize also let SVG
glyphs absorb the squeeze — a hard-narrowed pane rendered the design-system
palette icon tiny. Chips still shrink; their icons keep natural size.

* Bind Community Remix projects to the current workspace

Addresses mrcfps' review thread on apps/web/src/App.tsx:3303.

Product reproduction: 「通过 remix 后的方案, 不出现在我当前 workspace 里的
草稿了」 — stable repro in a team workspace.

`duplicatePluginAsProject` only sends the workspace/member identity headers
when its third `workspaceContext` argument is supplied, and the daemon's
`authorizeCreatedProjectWorkspace` deliberately reads a COMPLETELY headerless
create as a legal legacy/anonymous caller — `bindCreatedProjectToWorkspace` is
a no-op for a null context. Both remix call sites this PR adds (App.tsx's
standalone /community branch and EntryShell's community tab) omitted the
argument, so a remix performed inside a team workspace produced a project
bound to no workspace at all and therefore invisible to every workspace view.

Same enforceWorkspaceProjectMutation / created-project bypass class as
c0bce3b8f, fixed the same way: thread the already-resolved workspace context
that both files already hold, through the repo's existing
`resolvedWorkspaceContextForWrite` seam so an unresolved or unavailable
authority fails closed instead of silently creating an unbound orphan.

The second half of the review note — the follow-up `patchProject` whose `null`
was ignored — was a consequence of the same root cause: with the project
unbound, `enforceWorkspaceResourceMutation` finds no workspace row for the
caller's workspace and 403s the pendingPrompt seed, so the template prompt was
dropped too. Both requests now share one resolved authority, so they can never
disagree about which workspace they are acting in, and a still-null result is
reported instead of swallowed (without creating a second project, which is what
routing it through the existing catch would do).

Red spec first: apps/web/tests/components/community-remix-workspace-binding.test.tsx
drives both call sites in a team workspace against a fetch stub that reproduces
the daemon's create-then-bind contract. Red before this change (the create
carried no workspace headers, the project stayed unbound, and the seed PATCH
403'd), green after.

* Cover the refused-prompt-seed branch of the Community Remix fix

Second half of mrcfps' review ask on the same thread: prove what the flow does
when the follow-up `patchProject` still returns null after the binding fix.

The remaining null shapes are genuine transient refusals (daemon down,
membership revoked mid-flight, workspace locked between the two requests). By
then the copied project is real and bound, so the flow deliberately does NOT
fall into its own catch — that path creates a second, prompt-only project and
would strand the copy. It keeps the user on the remix and reports the dropped
seed. This case pins that: user lands on the remixed project, no second project
is created, and the failure is surfaced rather than swallowed.

* Re-anchor three style specs on the dogfooded UI

`Web workspace tests` went red on this branch because three CSS specs still
described the pre-dogfood layout the restyle replaced. Each one is re-pointed at
what shipped, keeping the invariant it was written to protect:

- `acceptance-visual-fixes` (recvpYDfW12NBu): the preset-thumbnail fix is the
  resting `scale(1.15)` framing transform, which is intact. Its hover partner
  went away with the family-wide cover-zoom removal (2026-07-27), so the spec
  now asserts the framing zoom on both media surfaces and separately locks the
  hover rules out — re-adding one would resample the covers soft again.
- `mention-popover`: the filter strip scrolls on one line instead of wrapping to
  a second row. The "no clipped labels" half of the contract is unchanged and
  still asserted (pills hold natural width, labels stay `nowrap`).
- `settings-polish`: the updater popup footer stacks the silent-update checkbox
  above a 50/50 action row. Long en labels must still wrap inside the checkbox
  column rather than overflow the panel, which the spec keeps checking.

* Return message-center focus to the rail opener that owns it

`hideTrigger` handed the message-center entry point to the rail but left the
component's internal `triggerRef` unattached, so `closePanel()` restored focus
to nothing. Opening focuses the portaled dialog, so every close — button,
backdrop, Escape, notification-settings — unmounted the focused node and dropped
keyboard focus to the document. Both external openers also failed to advertise
the dialog they own.

`MessageCenter` now takes a `returnFocusRef`: the host that hides the built-in
bell owns the duty that button used to serve. `EntryNavRail` points it at a
control that is still mounted after the close — the account trigger on the
signed-in branch, because the hover menu unmounts the 消息中心 row before the
panel opens, and the rail item itself when signed out. Both openers carry
`aria-haspopup="dialog"` and the live `aria-expanded`.

Red first: `EntryNavRail.message-center-entry.test.tsx` opens from each entry,
closes with Escape and with the close button, and asserts focus lands back on
the host control. All four cases failed before this change.

* Carry the menu popup contract onto the composer quick pills

The 插件 / 设计百宝箱 pills open the standalone `role="menu"` popovers in
ChatComposer, but they arrived without the contract of the ComposerPlusMenu rows
they replaced: no `aria-haspopup` / `aria-expanded`, no Escape handler, and no
focus return. Keyboard and screen-reader users could not tell a popup was open,
and Escape pressed while focus sat in the plugin search did nothing.

The pills live in ChatPane while the popovers live in ChatComposer, so the state
is plumbed both ways rather than duplicated:

- `onStandalonePanelChange` reports which popover is open, and each pill carries
  `aria-haspopup="menu"` plus its own `aria-expanded` — the same pair
  `ComposerPlusMenu` already puts on its trigger. No `aria-controls`: that
  surface does not use one, and this is not the place to invent a second
  convention.
- `openDesignToolbox` / `openPluginsPanel` take the opening pill as the
  return-focus target. Both popovers move focus inside themselves, so a
  dismissal has to hand it back, and the pill is the control the user came from.
- Escape closes through a document-level handler, mirroring ComposerPlusMenu's.

`dismissStandalonePanels` is deliberately only wired to the dismissal paths
(Escape, backdrop). Picking a plugin or an action keeps the plain setters,
because those hand focus to the composer input and pulling it back to the pill
would fight that.

No interaction changes: hover-open, the shared close timer, pill-to-popup
pointer travel and every selection path behave exactly as before.

Red first: `tests/components/ChatPane.quick-pill-popup-contract.test.tsx` runs
both pills through advertise / expand / Escape-from-inside / backdrop-dismiss and
asserts focus lands back on the pill. All six cases failed before this change.

---------

Co-authored-by: lefarcen <935902669@qq.com>

* Converge the visual Design Files prelude instead of clicking once (#6181)

`Playwright visual (settings-workspace)` went red on `[P2] captures the settings
BYOK surface` with a 10s `aria-selected` timeout: the Design Files tab sat at
`false` for all 23 polls with nothing re-clicking it. Seven other captures run
this identical prelude and passed, including the two neighbouring BYOK cases.

The defect is in the oracle, not the product. `prepareVisualWorkspaceFileList`
decided **once** whether to click — one instantaneous `isVisible()` probe of a
file row — and then asserted a state nothing retries. Any interleaving where the
workspace's own tab reconciliation lands around that single click leaves the
assertion permanently unsatisfiable for that attempt, and the lane runs
`OD_PLAYWRIGHT_FULLY_PARALLEL=1` with `retries: 0`, so it reports as a hard
failure rather than a flake.

`activateVisualDesignFilesTab` now converges on the goal state via `toPass`,
driving off `aria-selected` directly rather than through the file-row proxy —
the guard and the assertion were about different things even though
FileWorkspace derives both from one `activeTab === DESIGN_FILES_TAB` expression.
Repeat clicks are safe: the tab's handler is `setPersistedActive(DESIGN_FILES_TAB)`,
which is idempotent.

Why this is not a product regression from #6162: nothing in it touches tab
selection, tab persistence, or row visibility. Its only `design-files.css` change
deletes the hover cover-zoom, which cannot affect visibility, and a diff of
`apps/web/src` for `localStorage|activeTab|openTabs|restoreTab` comes back empty.
The lane's one green run before this failure (`4a6653186`, the #6174 repair of
the unrelated #5971 BYOK rename) is the whole basis for calling the baseline
clean.

Scope is the visual lane only — every caller is in `visual.ts` or
`visual-*.test.ts`, so the UI P0 lanes are untouched.

* fix(web): explain the AMR workspace-scope block instead of a dead send button (#6178)

An Open Design Cloud project run requires a resolved personal/team workspace
authority (21f452ffe). Failing closed there is correct and stays. Failing
closed SILENTLY was the bug: the send button went grey with no reason on
screen and no way out, while Home's equivalent dead end
(checkAmrBalanceGate -> AmrBalanceDialog reason `signed_out`) hands the user
an in-app sign-in. e2e/ui/amr-logout-requires-relogin.test.ts exists to
protect exactly that route, which a disabled composer can never reach.

Adds a classifier (`amrWorkspaceScopeBlock`) that turns "gate closed" into
the remedy that clears it, and a composer-adjacent notice that renders it:

  signed_out — the same AmrLoginPill action and `chat.amrBalanceGate.signInCta`
    copy Home's balance gate uses, so one identical action clears it from
    either surface.
  unresolved — a re-read of the project's workspace authority, exposed as
    `ProjectWorkspaceScopeState.revalidate`. An account action would be a guess.

The gate itself is untouched: `projectRunWorkspaceScopeReady` never consults
the classifier, and the `disabled: true` invariant stays asserted.

* test(updater): join the installer-reinstall floor across release and client (#6167)

* test(updater): join the installer-reinstall floor across release and client

A payload update never replaces the Electron outer shell, so a release whose
shell changed can only reach an old install through the installer. The
`control.launcher.version.{min,url}` floor is the one mechanism that forces
that, and it spans three owners: tools/release resolves the channel policy,
the feed carries it, and the desktop updater enforces it against the
physically installed outer version. Every side had unit coverage; nothing
joined them, so the only place a break in the chain would surface was a
release publish.

Adds six specs to the already-allowlisted packaged-launcher cross-boundary
suite, driving the real release-side resolver into a real feed and then into a
real packaged update check:

- an outer below the floor takes the installer, not the payload, and the
  launcher pointer proves nothing was adopted;
- the floor is judged by the installed outer rather than the payload it is
  running (two installs on the same payload, only the stale shell diverted);
- an unreadable installed outer fails closed as `outer-version-unreadable`
  with no installedVersion to render;
- a launcher schema beyond this client diverts on the ABI axis alone;
- with no floor configured a below-floor outer still swallows the payload,
  which is why a shell-changing release must set the repo vars deliberately;
- a floor above the release version stays refused.

Falsifying `remoteRequiresReinstall` to compare against `config.currentVersion`
-- the semantics the released 0.15.1/0.16.1 shells actually ship -- turns two
of these red, including the unreadable case being mislabelled as below-min.

Also pins the floor's operational shape against real channel version formats:
a bare stable floor sorts above every same-base `-beta.N`/`-preview.N`/
`-prerelease.N`, so inheriting it into those lanes hard-fails their publish.
The specs name that trap and pin the one-explicit-pair-per-lane configuration
that avoids it.

* test(updater): drive the reinstall floor on Windows too, off the real outer

Review (nettee) caught these specs running macOS only: the feed described
`platforms.mac`, the payload writer built a `.app`, and the updater config was
pinned to arm64/darwin. So `resolveInstalledOuterVersion`'s Windows branch —
`dirname(launcherLaunchPath)/resources/open-design-config.json`, a different
lookup from the mac bundle path — and the `installer` artifact shape never ran.
Since the installs most exposed to a shell/payload mismatch are the oldest ones,
leaving Windows uncovered left the half most likely to break unguarded.

The specs now run through `describe.each` over a per-platform target table
carrying the launch-path shape, outer-config location, installer artifact key,
and payload extractor.

They also stop using `OD_UPDATE_INSTALLED_VERSION`. That override returns before
the platform branch it was supposed to exercise, so it was masking the code under
test. Each scenario now materializes a real installed outer package at the real
per-platform location and lets the updater read the version off disk — which
also makes the unreadable-outer case genuinely unreadable rather than simulated.

Falsifying the Windows lookup (collapsing it into the macOS path) fails exactly
the two Windows specs that depend on reading a real installed version, while all
twelve macOS and publication-policy specs stay green.

* fix(web): partition the workspace plan nameplate like the wallet (#6182)

A workspace Vela Web shows as 免费 rendered as 专业版 Plus in the client's
account menu, while the 额度 row in the same card correctly read $0.00.

Money and plan travelled different paths. `GET /api/workspace/billing`
returns three independently scoped things, and only two of them are about
the workspace: `workspaceBalance` / `workspaceSnapshot` are proven for the
exact workspace + member, while `summary` is the caller's VELA ACCOUNT
billing (`workspaceId: null` by contract; the daemon reads it with one
unscoped `fetchBilling()` regardless of `?workspaceId=`). The rail's plan
nameplate read `response.summary` raw, so an account holding a personal
Plus reported `plus` in every workspace — a value that cannot change when
the workspace changes, because it was never about the workspace.

`workspaceBillingSummaryForContext` makes the partition key explicit, the
same `workspaceId` + `workspaceMemberId` pair money already uses:

  - personal workspace — the account IS the scope, summary passes through;
  - team workspace — plan comes from the context-authorized snapshot, and
    the account tier may stand in only when it is itself team-namespaced,
    since a personal tier cannot name a team workspace's plan.

That last clause keeps the 飞书 P0 fix intact: B omits planId/billingState
for a non-owner and `workspaceSnapshot` is an additive capability, so a
team-namespaced account tier is the only surviving evidence that a paying
MEMBER's team is subscribed.

`useWorkspaceBilling` is now this projection over the ambient context, and
EntryShell / SettingsDialog consume it instead of the raw summary. No
interaction, layout, or copy changes.

* Revert "fix(web): explain the AMR workspace-scope block instead of a dead send button (#6178)" (#6184)

This reverts commit ea259eb402.

* fix(daemon): resolve an unbound project against the caller's current workspace (#6185)

* fix(daemon): resolve an unbound project against the caller's current workspace

Every project must resolve to a workspace; when the project itself has no
binding, the workspace is the one the caller is currently acting in. There is no
other candidate.

`GET /api/projects/:id/workspace-scope` read only the persisted
`workspace_projects` row and the membership directory — never the request's
`x-od-workspace-*` identity — so a project with no row answered `unbound` for
every caller forever. `unbound` makes `projectWorkspaceScopeAuthorizesAmr` false,
which disabled the chat composer's send button for an Open Design Cloud run on
that project permanently, with nothing the user could do to clear it. #6178 wrote
user-facing copy for that state instead of fixing it (reverted in #6184).

`resolveProjectWorkspaceScopeForCaller` wraps the existing resolver instead of
branching inside it, so `resolveProjectWorkspaceScope` stays byte-identical and
the bound path is provably untouched. The fallback re-enters that resolver with a
synthetic binding naming the caller's own workspace, so the resulting
`workspaceMemberId` comes from the membership directory and never from the
request header: a caller can only select among workspaces their signed-in
identity is genuinely an active member of, and anything the directory cannot
confirm degrades back to `unbound` rather than to `unavailable` on a workspace the
project was never bound to.

Two cases keep today's behavior, both pinned by spec:

  unavailable — all three of its return sites. A project pinned to workspace X
    read by a member of Y answers X. The scope carries `workspaceMemberId`, the
    wallet that pays for the project's runs; resolving to Y would bill Y for X's
    project. Reachable because `GET /api/projects/:id` has no workspace gate, so
    a deep link or a workspace switch lands there.
  no caller identity — signed out, or a plain curl, has no current workspace to
    fall back to, and inventing one is worse than answering "none".

The fallback deliberately does not persist a binding.
`reconcileUnboundProjectBeforeMutation` was the preferred shape going in and is
wrong here: its own docblock is explicit that a passive read must not hand out
ownership just because it ran first, and this endpoint is a GET. Writing a row
from it would let whichever workspace opened the project first claim authorship
on no user intent, and would race two clients in different workspaces.

* fix(web): stop pre-emptively blocking an AMR send that has a wallet

An Open Design Cloud run is billed to the CALLER's own wallet, so the only
defensible client-side veto is "there is no billing principal at all". The gate
instead required THIS PROJECT's workspace scope to resolve, and blocked the send
whenever it did not: an unbound project, a membership-directory read that
transiently failed (offline / 504 / timeout all collapse into one `ok: false`
upstream), a workspace that is billing_past_due or locked, or a team the caller
has since left. In every one of those the user is spending their own quota.

It is also not the enforcement point. Real enforcement is server-side — the
daemon's `WORKSPACE_CONTEXT_REQUIRED` 401 plus vela's own billing check. The
client gate could only convert a request the server would have answered into a
dead, unexplained button, which is strictly worse than an honest server error.
It arrived as 21f452ffe, whose entire message is "fail closed on unresolved
workspace authority" with no body and no stated product requirement.

`workspaceIdentityCanBillAmr` names the invariant on the identity read, and the
gate now admits either witness of a billing principal: that identity, or a
project scope that already resolves to an explicit personal/team principal.
Strictly a widening — every state it newly admits was previously blocked, and
nothing previously admitted becomes blocked.

Deliberately NOT treated as "no wallet":

  loading — the identity read holds no answer yet. Reporting "signed out" on a
    frame that has not heard back is the bug shape this replaces.
  failure 'unavailable' — a transient outage taught us nothing about the user.
  failure 'unsupported' — an old daemon with no workspace endpoint keeps its
    legal pre-workspace behavior.

A genuinely signed-out caller — a settled, authoritative read that came back with
no workspace — stays blocked: there is no wallet, so the run cannot be billed and
cannot succeed.

No UI, no notice, no copy: #6178 wrote copy for this dead end instead of removing
it, and #6184 reverts that. This removes the dead end.

* fix(web): point every console entry at /dashboard, drop the wallet target (#6165)

* fix(web): point every console entry at /dashboard, drop the wallet target

Product ruling: there is no wallet page in the console's information
architecture any more. Balance, manual top-up and the auto-recharge policy were
all rehomed onto the console dashboard (vela #1055), so every entry that used to
open `/wallet` now dropped users on a surface the product no longer navigates
to.

Retargets all of them:

- `AMR_CONSOLE_URL` / `DEFAULT_AMR_RECHARGE_URL` and the per-profile console
  map (prod / test / local) now resolve `/dashboard`, keeping the
  `?source=open_design` attribution param.
- `teamConsoleUrl`'s `billing` section — the team account menu's 「额度 $0.00」
  row — is a plain dashboard visit.
- `teamConsoleUrl`'s `plans` section, `amrPlansUrlForProfile` and
  `amrPlansUrlForWorkspace` — the free-tier 「升级」 button and every other
  Upgrade affordance — send `billing=plan` instead of the wallet page's
  `view=plans`.
- `amrWalletUrlForWorkspace` is renamed `amrConsoleUrlForWorkspace` so the name
  matches where it now points.

`billing=plan` is the console's one state-aware upgrade intent: its dashboard
resolves it against the workspace's real subscription state, so a personal owner
lands on the personal plan modal — the dialog the console's own 「升级订阅」 hero
button opens, which is what product asked the free-tier 「升级」 button to reach —
while a team owner still gets checkout or change-plan. The team `upgrade` section
keeps sending its more specific `billing=checkout` hint, but that is now only a
hint: powerformer/vela#1127 makes the console fall back to the dialog that
actually matches, so a stale `hasActivePlan` can no longer strand the user on a
bare Overview page (recvpSQKna0LwR).

Comments carrying earlier live-verification findings are updated wherever this
change invalidated them, rather than left contradicting the code.

No new user-facing copy, so no locale changes: the affected labels are all
action-named (「充值」/「升级套餐」), never destination-named.

* fix(daemon): retire the wallet target in the AMR recharge link too

The daemon builds its own recharge URL for AMR balance failures — it lands in
the failure message and in `actionUrl`, which the client renders as the chat
error card's clickable link — and it still pointed at the console's retired
wallet page. Same product ruling as the client-side retarget: balance and manual
top-up report on the console dashboard now (vela #1055), so this link has to go
there.

Every existing assertion in `vela-errors.test.ts` referenced the constant
symbolically, so all of them would have kept passing while the link sent users
to a surface the product no longer navigates to. Added one that pins the literal,
which is the red spec for this change.

Also retargets the two e2e assertions that pinned the old destination
(`e2e/tests/amr/insufficient-balance.test.ts` on the daemon's emitted URL,
`e2e/ui/visual-workspace.test.ts` on the avatar upgrade link's `view=plans`), and
the web-side fixtures that mimic the daemon's emitted string so they stop
documenting a URL it no longer produces.

* chore(release): bump feat/workspace-team to 0.16.2 (#6188)

The published beta feed is self-inconsistent: metadata.json carries
baseVersion 0.16.1 alongside betaVersion 0.16.2-beta.144, because
0.16.2-beta.144 was published with an explicit release_version override
while every manifest on this branch still said 0.16.1. Every subsequent
beta build now fails its own consistency check:

  [release-beta] beta metadata.json baseVersion 0.16.1
                 does not match betaVersion 0.16.2-beta.144

`force` does not help — it only waives "base must exceed latest stable",
not the baseVersion/betaVersion match.

Raising the branch's base version to 0.16.2 makes the next beta
(0.16.2-beta.145) consistent with its own metadata, and removes the need
to pass force at all. Same 18 manifests the release bot touched in
23f9fcaac; test fixtures that mention 0.16.1 as data are left alone, as
that commit also left them.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(collab): keep a project's "last changed" time out of sync's hands (#6189)

A member who opened the client hours later and pulled a shared project saw
「刚刚更新」 on a project nobody had touched, and the home 最近项目 strip kept
rendering the previous workspace's cards for a beat after switching workspaces.

Two independent defects, both reported by the product owner on a packaged
client.

1. `projects.updated_at` (and the `workspace_projects.updated_at` the project
   list folds into it with `MAX`) is what the project card shows and what the
   list sorts by, so it must answer "when did a person last change this
   project's conversations, files, or name". Sync was answering it too:
   `materializePulledTeamMirror` carried the origin's timestamp into the
   project row but not into the binding it wrote in the same transaction, so
   `MAX` surfaced the pull's own clock on EVERY pull. Four more sync writers
   had the same shape. `SYNC_KEEPS_UPDATED_AT` names the invariant and the
   sync paths now either carry the origin's time or keep the row's existing
   answer.

2. A loaded project list describes exactly one workspace identity, so leaving
   that workspace invalidates it. The re-list on switch runs in an effect —
   after the commit — so the browser painted workspace A's cards under
   workspace B's identity first. The list is now dropped during the render
   that first observes the new scope, so no frame shows another workspace's
   projects. No extra request: the refetch already existed and was already
   keyed on the resolved workspace.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): key every workspace read cache on who is asking (#6198)

`coalesced-get` and `shared-cancellable-get` are CACHES (1s share window), not
just in-flight dedupers, so a key that omits the caller's identity hands the
previous workspace's answer to the next one — and an in-flight entry is joined
with no window at all.

Three reads were keyed on a constant:

- `useTeamMembers` — `'workspace-members'`. `GET /api/workspace/members` answers
  for whichever workspace the DAEMON is active in and reads nothing off the
  request, so the roster is a per-workspace answer with no per-workspace URL.
  A leaked roster renders the previous workspace's teammate name on project
  cards (`RecentProjectsStrip.resolveCreator`). Now keyed on the workspace
  identity, which also makes the hook re-read on a switch instead of waiting out
  its 15-60s poll, and a late read for the identity the user left no longer
  overwrites the newer roster.

- `useWorkspaceContext` — `'workspace-context'`. This is the read that
  ESTABLISHES the identity, so it cannot be keyed on the identity it fetches and
  has no workspace argument to borrow (the switch is a separate
  `PUT /api/workspace/active`). It is keyed on the identity GENERATION instead:
  one token per deliberate identity change, so a single broadcast heard by a
  dozen mounted consumers still collapses to one request, while two switches
  inside `forceCoalescedGet`'s 250ms burst window stop being mistaken for one.

- `useProjectWorkspaceScope` — `project-workspace-scope:<id>`. The scope carries
  the `workspaceMemberId` that pays for the project's runs, so a shared key
  serves one member the wallet of another.

The same read also fetched HEADERLESS, which made #6185's daemon fallback
unreachable: `GET /api/projects/:id/workspace-scope` resolves an unbound project
against the caller's current workspace, but only when the request carries the
caller's `x-od-workspace-*` identity. It now sends `workspaceProjectHeaders`,
the same set every other workspace-aware project read uses — and waits for the
caller's own identity to settle first, so a project open still costs one request
instead of a wrong headerless one followed by a corrected one.

* fix(collab): carry B's workspace name for personal workspaces too (#6199)

B names EVERY workspace, personal included: vela's CurrentWorkspaceContext
requires `workspaceName`, derives "«owner»'s workspace" for an unnamed
personal one (vela #964), and an owner may rename it outright.

The daemon read that field and then threw it away unless the workspace was a
team, because the only name on WorkspaceCollabContext was `teamName`. The
switcher's collapsed label therefore had nothing real to show for a personal
workspace and fell back to a hardcoded English "Personal workspace" until the
user opened the dropdown and the separate workspace-directory read landed.

Adds `workspaceName` to the context contract and carries it for both workspace
types, in the vela mapping and in the directory-item synthesis. `teamName`
stays team-only: it is the team switcher's field and doubles as an "is a team"
signal, so nothing may start reading it for a personal workspace.

The web side consumes the context the shell already holds at startup, so the
label is correct on first paint and NO new request is added — the diff adds
zero fetch call sites. The dev provider parses `workspaceName` too so the
dev/demo lane can drive a personal workspace's label the way B does.

Note this is not what the reported screenshot shows: there, B's real name for
that workspace IS the literal "Personal workspace", because an in-place team
upgrade flips the default workspace's stored type to 'team' and vela's
owner-derived naming then no longer applies. That half is upstream.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): stop offering 升级 at the top plan tier (team_max) (#6205)

A 团队版 Max workspace is the top of the ladder — nothing is above it — but
both upgrade entry points still showed 「升级」, and clicking it could only
reopen the plan the user already holds.

The two surfaces got there two different ways, and neither shared a rule:

- The account menu's billing card gated on `billingUpgradeUrl &&
  canManageBilling` — a destination check and a permission check, with no
  TIER check at all. Every tier saw the button.
- The Settings AMR card asked a personal-ladder question
  (`canUpgradeVelaPlan`) about `amrCardStatus.account.plan` — vela's
  ACCOUNT-scoped login projection, which reports `free` for a user whose
  entitlement is held by a team workspace. So a team_max owner measured as
  "free" and got the button, while the PlanBadge beside it correctly read
  Max off the workspace-resolved tier.

Both now ask one predicate, `canUpgradeFromPlanTier`, about the same
resolved tier each surface already LABELS, so the button can never
contradict the nameplate next to it.

Team and personal tiers are two ladders, so the predicate reads the
team-namespaced id whole instead of stripping the prefix and asking a
personal question. Per the product ruling — 「个人档位都是要显示可升级的,
最顶的就是团队 max」 — personal `max` keeps the affordance: that user can
still move onto a team plan, and vela #1146 deliberately routes their
click to the Team upgrade dialog. Only `team_max` loses it.

Destination logic (`workspaceUpgradeUrl` / `amrPlansUrl`) is untouched;
this only decides whether the affordance renders. No copy, no new i18n
keys.

* fix(web): keep Send alive after using a community template (#6206)

Clicking 「使用」 on a community template seeds the template's own brief into
the composer and binds the template as the run's driver. For a template whose
`od.inputs` are `required: true` with no `default` -- the real
`example-html-ppt-pitch-deck` ships three such fields -- the bind left
`active.inputsValid` false, and `submitDisabled` folded that into the send
button. The user saw a full prompt next to a permanently dead Send whose
tooltip still read "type something to run". Typing did not help: the gate was
never about the prompt.

Those fields have no input surface on that path. #3645 removed the Home
composer's inline plugin-inputs form, and the template's query carries no
`{{...}}` placeholders, so prompt -> inputs write-back cannot reach them
either. A gate on values the user cannot supply anywhere is a dead end, not a
guard.

Narrow it to where it is actionable: `requiredInputsAreUserFillable` keeps the
gate when the media composer renders the fields as controls, or when the
tracked query template actually names the missing field as a `{{placeholder}}`
that editing the prompt writes back. A tracked template alone is not enough --
re-applying a plugin without an explicit template derives one from the
manifest, placeholders or not, which is the shape a reload lands in.

The daemon stays the authority: `validateInputs` still throws
MissingInputError. Since the seeded path no longer pre-empts the send, its
rejection now reports the fields it rejected instead of a generic "Failed to
apply", reusing the existing pre-submit wording.

* fix(collab): give every created project a workspace home (#6201)

* fix(collab): give every created project a workspace home

A project created with no `x-od-workspace-*` headers got no
`workspace_projects` row and still returned 200, so nothing surfaced the
loss. `GET /api/projects/:id/workspace-scope` then answers `unbound`,
which strips the workspace off the run request (`ProjectView`'s
`projectRunWorkspaceContext` -> an Open Design Cloud run nothing can
bill) and blanks the balance/plan area while that project is open
(`AvatarMenu`). The same orphan is also denied its first run outright by
`enforceWorkspaceResourceMutation`.

Headerless creates are not anonymous callers asserting "no workspace" —
they are callers that could not say which workspace they meant:
`od project create` and the MCP `create_project` tool mint no headers at
all, the web duplicate / design-system-copy / plugin-share paths read a
ref that drops the context's `loading` flag, and Orbit + scheduled
routines have no request to read headers from. So a create that names no
workspace now binds to the one this daemon is signed in to, read through
`workspaceContext.lastKnown()` — zero-network, synchronous, no failure
mode. A daemon that has resolved no workspace still binds nothing, which
keeps `unbound` correct for the signed-out single-player user.

Six creation paths were writing no binding at all, not merely skipping it
when headerless: the plugin share-project task, Orbit run projects,
scheduled routine projects, the library capture-as-page exit, the
project-location scan importer, and the `ds-*` project backing a design
system's editing workspace. All six now bind.

Deliberately unchanged: the collab pull's register-on-pull and
shared-project placeholder. Those are a teammate's shared project, whose
correct binding is written by `materializePulledTeamMirror` with
`visibility: 'team'`; claiming them into the reader's ambient workspace
as personal would misattribute someone else's resource.

No creation path gains a new failure mode. `createdProjectWorkspaceHome`
never refuses — a partial or denied header identity degrades to the
ambient workspace rather than turning a working 200 into a 4xx.

On the web side `createPluginShareProject` was the one create in
`state/projects.ts` with no `workspaceContext` parameter at all, so it
sent no workspace identity ever. It now forwards the context when known,
best-effort — deliberately not `resolvedWorkspaceContextForWrite`, which
throws while the identity read is in flight and would add a new block to
a path that works today.

* fix(collab): verify an asserted workspace before persisting it

`createdProjectWorkspaceHome` resolved a created project's workspace with
`resolveCreatedProjectWorkspace`, which only PARSES headers. It trusted
the caller-provided member status and permissions and never checked that
the workspace/member pair exists in the membership directory or agrees
with the daemon's last-known state. `x-od-workspace-*` headers are an
unauthenticated hint, so any local caller — the `od` CLI, a plain curl, a
compromised page — could assert an arbitrary pair and get a project bound
to a workspace it has no membership in, with a fabricated
`createdByWorkspaceMemberId`.

"Do not fail the create" and "trust unverified input" are not the same
constraint. The helper now VERIFIES, then DEGRADES — never rejects, never
trusts:

  1. asserted identity that verifies      -> bind to it
  2. asserted identity that does NOT      -> bind to the daemon's ambient
     verify, including an unreadable         workspace instead, or leave
     authority ("cannot confirm")            the project unbound
  3. nothing asserted                     -> ambient, as before

Creation still never answers 4xx, so the six previously-never-bound paths
keep binding and a signed-out daemon still creates usable local projects.
An unverifiable claim is simply never written as fact.

Verification is not re-implemented. It is `authorizeCreatedProjectWorkspace`
— the same directory lookup `POST /api/projects` gates on, which also
returns the DIRECTORY's authoritative context rather than the caller's
claimed one — plus `withLastKnownMembership`, the same cross-check the
mutation gates apply (now exported rather than copied). Only the failure
behavior differs: they refuse, this degrades. An absent directory fetcher
remains `authorizeCreatedProjectWorkspace`'s own documented local/dev
compatibility path, not a second weaker definition of verified.

Route modules now take one `CreatedProjectWorkspaceResolver` dep instead
of re-threading the three authorities, so there is a single notion of
"verified" per daemon.

* test(daemon): drive brand routes through the created-project workspace resolver

The brand harness constructed `registerBrandRoutes` without the new
`resolveCreatedProjectHome` seam, so the binding became a no-op and
"binds a freshly extracted brand project into the caller's ACTIVE team
workspace" went red. Inject the same production resolver `server.ts`
builds, in its no-directory (local/dev) configuration, so the test keeps
asserting the product behavior through the real seam instead of a
header-trusting shortcut.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): move the update-ready rocket above the rail account row (#6208)

* fix(web): move the update-ready rocket onto the rail account row

#5517 removed the entry topbar and parked the updater host in the rail
footer, which put the green rocket on its own line UNDERNEATH the account
row — bottom-left, detached from the identity it belongs to. Product
reported it from a real client: the rocket belongs at the right edge of
the name + plan wordmark line.

Move the mount instead of nudging it with CSS: the rail now owns the
updater host through an `updaterSlot` prop and renders it inside the
account row (`.entry-nav-rail__account-row`), as a SIBLING of the account
trigger so it never steals the account-menu click target. The rail footer
stays as the fallback home for the signed-out shell, which has no account
row to ride, so the rocket's appearance conditions are unchanged.

Same change also un-clips the 「更新已就绪」 prompt. The host is pinned to
the bottom of the rail, so a `top: 0` panel grew downward off the bottom
of the window and the copy was cut off mid-sentence. Anchor its BOTTOM
edge to the host instead, the same way `.entry-help-popover` already does
for the same reason.

Side effect worth having: the rail no longer grows a 52px footer strip
the moment an update lands, so the account row stops jumping.

* fix(web): put the rocket ABOVE the account row, right-aligned

Placement correction. 「放到名字啥的上面的右侧」 was read as "on the name
row, right side"; product clarified 「应该是在名字那个横条的上方,靠右对齐」
— the rocket gets its own right-aligned strip ABOVE the identity row, not a
slot inside it.

The strip (`.entry-nav-rail__account-updater`) is mounted unconditionally so
it stays the account trigger's immediately-preceding sibling, and carries
`:empty { display: none }` so an idle rail reserves no height for it. That
rule is load-bearing: without it every user without a pending update would
get a blank 34px row plus the container's 6px gap pushing the account area
up — a worse regression than the placement it fixes. Measured in a real
browser at 1280x900: the account trigger sits at y 850-884 in BOTH states,
so nothing shifts.

Everything else from the first commit stands: the rail owning the host via
`updaterSlot` instead of an opaque `footerExtra`, the sibling-not-child
relationship to the trigger (now moot in the best way — a strip above the
row cannot be button-in-button), the `bottom: 0` panel anchoring that
un-clips 「更新已就绪」, and the footer fallback for the signed-out shell.

* fix(web): stop the home model list offering picks the chip cannot honor (#6209)

On the Home composer, clicking a model in the chip's compact list did
nothing — the chip kept showing the previous model.

The compact list rendered `currentAgent.models` raw, so a model above the
caller's plan (`enabled: false`, straight from `vela model list --json`)
appeared as an ordinary selectable row. Clicking it wrote the pick to
config, `normalizeAgentModelChoice` reported the plan fallback instead, and
the re-normalization effect in `InlineModelSwitcher` persisted that
fallback — so the chip snapped back with no explanation, and the user's
previous pick was overwritten by the default in the process.

The sibling surface (`AvatarMenu`, the project composer's model list) had
already hand-rolled `agent.id === 'amr' && model.enabled === false` for
this. Copying the rule per surface is what let the newest list drop it, so
this introduces one definition instead: `agentModelIsSelectable`, defined
to be at least as strict as `normalizeAgentModelChoice` (pinned by a test)
so a gated row can never be a pick the config would revert. Both surfaces
now ask it, and every model write inside `InlineModelSwitcher` — the
compact list and the execution-settings picker — goes through a single
`applyAgentModel` sink that asks the same question, leaving no second path
for a future list to bypass.

Locked rows now read as locked: dimmed, `aria-disabled`, a lock glyph, the
existing `settings.amrModelUpgradeHint` reason, sorted below the available
models, and a click routes to the plans page instead of silently doing
nothing.

* fix(collab): verify the asserted workspace before claiming an orphan project (#6213)

* fix(collab): verify the asserted workspace before claiming an orphan project

`reconcileUnboundProjectBeforeMutation` claims a project this daemon has
never bound to ANY workspace into the current mutating request's
workspace, so a true orphan is not denied its first mutation by
`enforceWorkspaceResourceMutation`'s `!row -> false` rule (recvqbhor3pai2).

It resolved that workspace from `workspaceProjectContextFromRequest(req)`
— header PARSING with no authority check — and stamped
`createdByWorkspaceMemberId: ctx.workspaceMemberId` from the same headers.
`x-od-workspace-*` is an unauthenticated hint any local caller can forge,
so a plain curl could claim someone else's orphaned project into a
workspace it has no membership in AND install itself as the author.
Authorship is the dangerous half: `workspaceResourceAccess` derives
`selfCreated` from it, and that bit is what grants a non-privileged member
mutation rights over the row.

Both the workspace and the authorship now come from
`resolveCreatedProjectHome` — the same verify-then-degrade resolver #6201
introduced for created projects:

  - asserted identity VERIFIES against the membership directory -> claim
    it, attributed to the DIRECTORY's member id, not the header's;
  - it does NOT verify (foreign, inactive, permissions disagree,
    last-known says removed, or the authority is unreadable so it cannot
    be confirmed) -> claim the daemon's own ambient workspace instead,
    attributed to that verified session's member id;
  - neither available -> write nothing, and let the pre-existing gate
    answer.

The `null`/`'missing'` early return is unchanged and load-bearing, which
is why this does not reuse `createdProjectWorkspaceHome`'s own third
branch: the mutation gate's HEADERLESS branch answers 401 as soon as any
row exists for the resource, so claiming on a request that asserts
nothing would convert a working headerless mutation into a 401.

A verified caller is unaffected — `workspace-projects.test.ts`'s
recvqbhor3pai2 regression case still passes unchanged. A caller that
cannot prove membership over a never-claimed project is now refused by
that pre-existing gate instead of being let through on a binding it had
just invented for itself; #6201's e2e case is updated to assert the
persisted binding rather than the mutation's status, since both outcomes
satisfy the property it pins.

* fix(collab): persist only an asserted-and-confirmed workspace on an orphan

Review follow-up (mrcfps). `resolveCreatedProjectHome` is not a pure
verifier: by contract a foreign, inactive, removed, or unconfirmable
assertion degrades to `workspaceContext.lastKnown()`. Reconciliation then
persisted that ambient context onto a PRE-EXISTING orphan before the
mutation gate had rejected the asserted caller — and the
`getWorkspaceProjectByProjectId` guard one line above makes that write
sticky, so the rightful verified workspace could never reconcile it
afterwards. A forged request, or a legitimate one during an authority
outage after the active workspace changed, would permanently mis-file the
project.

Creation and reconciliation are not the same risk, which is the
distinction the original patch missed. Putting a BRAND-NEW project in the
ambient workspace takes nothing from anyone — that is #6201 and it stays.
Putting an EXISTING orphan there may take it from the workspace it
actually belongs to.

So a degraded ambient authority is persisted only when it names the very
pair that was asserted. Verified assertions resolve to the asserted pair
by construction (the directory lookup is keyed on it), so they are
unaffected; outage continuity still works for the ordinary legitimate
caller, whose client took its headers from this same daemon and therefore
agrees with it; a mismatch writes nothing at all.

Not in tension with 「所有 project 都应该能找到 workspace」: the
display-side fallback (#6185) already renders an unbound project inside
the caller's current workspace, so nothing reads as "unknown". The
binding row is a separate, durable fact and stays conservative.

Adds the fixture-matrix case the review asked for — a populated but
DIFFERENT ambient workspace, asserting the orphan stays unbound. The mock
authority's `/current` is now flippable so the fixture can create a true
orphan while ambient is absent and then populate ambient deterministically
through `GET /api/workspace/context`, rather than depending on a poll tick.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): use neutral Windows app background (#6218)

* fix(collab): let a headerless caller mutate its own daemon's project (#6216)

* fix(collab): let a headerless caller mutate its own daemon's project

`enforceWorkspaceResourceMutation`'s headerless branch answered
401 WORKSPACE_CONTEXT_REQUIRED as soon as ANY `workspace_projects` row
existed for the resource. That was survivable while headerless creates
left projects unbound. #6201 made every created project get a workspace
home, so the two rules combined into a project its own creator cannot
touch:

    od project create     -> 200, and now writes a binding
    od project duplicate  -> 401

The `od` CLI never sends `x-od-workspace-*` — only `od workspace …`
builds those headers — so this is not an edge case for it. It is every
CLI mutation of every project the CLI created, which breaks the
embeddability contract `AGENTS.md` makes the CLI responsible for.
Reproduced end to end through the real binary against a daemon-only
process (`od daemon start --headless`, no web, no browser).

INVARIANT, now named in `headerlessMutationAllowed`: every mutation
resolves to a workspace identity. A request that ASSERTS one is judged on
that assertion; a request that asserts NOTHING is the local daemon's own
signed-in user and is judged as that identity. Being unable to name a
workspace is not the same as having no standing in one — the same
"nothing asserted -> ambient" fallback the create path already applies,
so the gate and the creation paths finally agree about what a headerless
caller is.

Verified the ambient identity is available without any UI: a
daemon-only process resolves it from the vela session on disk, and
`GET /api/workspace/context` answers fully in that state. The
`ambientWorkspaceId` in `collab/workspace-hub-subscriptions.ts` is
unrelated — it is SSE subscription bookkeeping fed from the
disk-persisted selection store, not an authorization input.

Deliberately NOT weakened: `authorizeCreatedProjectWorkspace` still
refuses to let ambient stand in for an explicitly CLAIMED pair, and
`resolveProjectWorkspaceScope` still resolves a persisted binding without
consulting ambient. Both govern cases where something WAS asserted;
headerless is the third case.

What stays refused, because the original branch protected something real
(recvqbeDjAsejl / recvqbklNGDqYY, spec 04 §10): a resource bound to a
workspace the daemon is not currently in; a resource the daemon's own
identity may not mutate anyway (the same
`workspaceResourceMutationAllowed` computation runs); and everything when
the daemon has no signed-in identity to resolve. With no ambient wired
the branch is byte-identical to before.

The ambient snapshot type moves down into
`collab/workspace-resource-mutation.ts` so the gate and the creation
paths cannot drift into two notions of "the daemon's own workspace";
`created-project-workspace.ts` re-exports it and now reuses the shared
context builder instead of its own copy.

* test(e2e): pin that signed out, visible equals unbound equals mutable

「未登录也可以用自己 cli 修改未登录态下的那些 project, 这个一定要保证,
不然就是 P0 事故」

A signed-out client can only SEE unbound projects: the no-scope catalog
(`routes/project/index.ts:2432-2443`) reads no `x-od-workspace-*` at all
and joins through `listUnboundProjects`, so a project any workspace has
claimed never leaks to a caller with no identity to check it against
(spec 04 §10). This pins the other half of that set — everything visible
while signed out stays WRITABLE.

The case runs against a daemon with NO vela session at all, not merely a
request without headers, creates its draft in that same signed-out state
(the real user sequence), asserts the draft is listed by the no-scope
catalog, then renames it, duplicates it, and duplicates it again through
the real `od` binary.

It holds only because `headerlessMutationAllowed` short-circuits on "no
row anywhere" BEFORE it asks for an ambient identity, so it is
mutation-tested against that ordering rather than written red-first:
moving the short-circuit after the ambient check turns it red with
`expected 401 to be 200`.

* fix(collab): make the delete side effect use the gate's own context

Review follow-up (mrcfps). Allowing a headerless caller to mutate a
project the daemon's own identity owns made a latent inconsistency
reachable: `DELETE /api/projects/:id` re-derived its context with
`workspaceProjectContextFromRequest(req)`, which is null for a headerless
caller, so `requestTeamVisibility(..., 'personal')` was skipped while
`dbDeleteProject` still ran. The owner's local row and directory
disappeared and the hub kept serving the resource, so teammates went on
seeing a project that no longer existed — a cross-client data-consistency
bug, worse than the 401 this branch set out to fix.

Threading the context, not narrowing the capability:
`effectiveWorkspaceProjectContext` names the identity a mutation actually
acts under — asserted when the request carries one, ambient when it does
not — and its docblock states the invariant that any SIDE EFFECT must read
from it rather than re-deriving from headers, or the effect disagrees with
the gate that permitted it. The delete now uses it.

The no-context case becomes a refusal rather than a silent skip. It is
unreachable while the gate is intact (a team-bound row is admitted only
for an asserted identity or a matching ambient one), but falling through
is what produced a local-only delete of a still-shared project, so a
future gate change must not be able to reintroduce it quietly.

Audited every other capability this branch newly admits for a headerless
caller. Delete was the only one: `/move` and both share/unshare routes
each guard with their own `if (!ctx) return sendMissingWorkspaceContext(res)`
so headerless never reaches their hub work, and the upload route's
`workspaceProjectContextFromRequest(req) !== null` only shapes a 404 for a
missing project. Rename/duplicate/writeFiles/comment have no hub side
effect.

Adds the requested route regression to
`tests/routes/project-delete-unshares-team-share.test.ts`: real route,
real collab runtime, mutable fake hub, no request headers,
`workspaceContext.lastKnown()` populated — asserting the unpublish and
catalog removal happened and the project is gone locally only after. Plus
the other half: with no ambient identity either, the delete is refused and
nothing is destroyed. Mutation-tested — restoring the header re-derivation
turns the first red with "expected 0 to be greater than 0".

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(web): draw the team wordmark for every tier of a team plan (#6219)

The account row of a team workspace showed the PERSONAL Plus wordmark. Product
ruling: 「团队版的订阅,这里应该都显示 team 的标识」…「产品期望团队从 free 到
max,徽标都显示 team 的那个,个人的还是维持现状不要动」.

The badge names the plan FAMILY, not the tier inside it, and it got that wrong
from both ends. `planBadgeTierForLabel` asked `plus` / `pro` / `max` BEFORE
`team`, and B namespaces team ids by EMBEDDING the personal tier word — so
`team_plus` matched `plus` and the `team` branch was reachable only from a bare
`team` id, the one team tier that happened to work. The free team tier missed
from the other side: B reports an unsubscribed workspace with a null `planId`
and an empty `membershipTier`, an id no different from a personal free account,
so no id can name it.

`planBadgeTierForWorkspace` states the whole rule once, and both surfaces read
it — the rail's account row and the Settings CLI card, which the owner requires
to move together (「设置中的这里应该一样的逻辑」). Team-namespaced ids go through
the shared `isTeamPlanTier`, so the badge cannot drift from the other team
gates; `workspaceType === 'team'` covers the free tier that no id can express.

The plan LABEL is untouched, deliberately: it answers a subscription question,
every user-created workspace in B is team-typed, and reading one as the other
is #146. 免费 beside the `team` wordmark is now the intended pairing — the
family is team, the subscription is not paid.

No interaction, layout, sizing, artwork, or copy changes; no new i18n keys.

* fix(collab): warm the workspace-scoped digest faces on switch and reconnect (#6222)

Every workspace-scoped cache in the daemon keys on the active workspace, so the
instant the user switches, `catalog` and `members` are both a miss and the FIRST
consumer in the new workspace refills them inline on its own request path
(`createPersistentSyncCache` fetches synchronously on a miss). The cost is the
same either way; the only question is who waits for it. A switch is a user
action followed by idle time, so paying it there is free — paying it inside the
first project load or agent run is not.

`PUT /api/workspace/active` now announces a CONFIRMED switch through a new
fire-and-forget `onWorkspaceSwitched` seam, and the daemon warms the two faces
from it. The announcement deliberately does not fire for a rejected or
rolled-back switch: warming there would refill the caches against a workspace
this daemon just refused to move to.

Warming goes through the cache functions, never their underlying fetchers.
`createSwrCache`'s read returns `entry.inflight ?? refresh(key)`, so a warm that
races the very consumer it is protecting joins that consumer's request instead
of issuing a second one. No request is added that would not otherwise be made.

Second half, same root cause: on hub reconnect the gap-closing catch-up read the
team-project and member lists THROUGH those same caches, so an entry that
settled just before (or during) the disconnect still counted as fresh, the diff
concluded "nothing changed", and the one cycle meant to close the gap emitted
nothing — the client stayed stale until an unrelated read happened to miss.
Reconnect now drops those two entries first, and `pollOnce` joins the in-flight
refresh that starts, so the pair still costs one fetch per face.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(daemon): split AMR workspace proof failure from proof refusal (#6221)

An AMR run died in preflight with "its persisted workspace <id> is
unavailable for the signed-in member". Two causally unrelated situations
shared that one error and one outcome: a directory that answered and
genuinely listed no active membership, and a directory that could not be
read at all. The `.catch` swallowed a read failure into
`{ ok: false, items: [] }`, so a transient vela blip made a user's own
project unrunnable — and nothing downstream could tell the two apart.

Refusal now fails closed under `ProjectWorkspaceScopeRefusedError`
(`PROJECT_WORKSPACE_SCOPE_REFUSED`); an unreadable directory is retried
with bounded backoff and, if it still cannot be reached, the run proceeds
with no workspace, i.e. on the caller's own account wallet. That is a
product decision recorded in the docblock: a hard failure is worse than a
mis-attributed personal charge. The reverse direction, charging a personal
run to a team wallet, stays impossible — no fallback selects another
workspace, and a proven non-membership still refuses.

The decision was also invisible: a full diagnostics export carried no
directory-fetch line, no run-id in the daemon log, and only a generic
AGENT_EXECUTION_FAILED plus prose. Every branch now reports a closed-union
outcome to the daemon log, the run record (via `ApiError.details`, leaving
the wire error code unchanged) and telemetry.

* fix(comments): preserve rejected mutations

* fix(comments): preserve partial send results

* fix(web): keep workspace-scoped data fresh across account and workspace changes (#6225)

* fix(web): keep workspace-scoped data fresh across account and workspace changes

Three defects in one family — workspace-scoped data in the web client going
stale or being read under the wrong identity.

1. The workspace switcher's module-level directory cache had no invalidation.
   `/api/workspace/directory` answers "which workspaces can the SIGNED-IN
   ACCOUNT see", but nothing cleared the cache on an account change and
   `resetWorkspaceDirectoryCache` had no production caller — so signing in as a
   different account kept the previous account's workspace names on screen, and
   kept them confidently, since a non-empty cache also suppresses the loading
   state. The cache is now served only to an identity that can claim it, by
   membership id, which keeps the flash-free reopen it exists for. The
   `coalescedGet` key is identity-scoped for the same reason.

2. App.tsx read `/api/skills` without the caller's workspace headers and never
   refetched on a workspace switch. The daemon is fail-closed on a missing
   `x-od-workspace-id`, so a headerless read is not the unfiltered list — it is
   the list with every workspace-claimed skill removed, including the ones
   claimed by the workspace the user is in. Skills now get the identity headers
   and the same workspace-keyed refresh design systems already had. Launch still
   spends exactly one `/api/skills` request: the read moved from the boot pass to
   the workspace-keyed effect rather than being added alongside it.

3. `PUT /api/workspace/active` already returns the post-switch context, from the
   same producer `GET /api/workspace/context` serves and only after the daemon
   agrees the switch happened. The client parsed that body, discarded it, and
   made every consumer re-read the same data. The shell is now seeded from the
   response; the refresh broadcast still fires, because
   `useProjectWorkspaceScope` listens to it and passive tabs cannot be seeded.
   Switch path: 2 requests -> 1.

* fix(web): discard workspace-scoped reads that land after the identity moved

Review follow-up on #6225 (PerishCode, two threads — both real).

Keying a read by identity stops the wrong identity being SERVED a cached
answer. It does nothing about the other direction: a read issued for identity A
resolves later, and by then the caller may be identity B. Committing that late
answer restores A's data under B — the same staleness this PR removes, arriving
through the back door. A workspace switch is precisely when one read is in
flight and another starts, so reverse-order completion is reachable, not exotic.

Both sites now use one mechanism, `beginWorkspaceScopedRead`: capture the
identity the read is issued FOR, request with that captured context, and compare
against a ref before any commit. Its docblock states the invariant and the two
rules that make it hold (request with the captured context so request and guard
cannot disagree; compare against a ref, never a closed-over prop, which would
always match and guard nothing). It is the cross-component form of the
`requestEpochRef` ordering guard `useWorkspaceContext` already applies to its
own read.

- `refreshSkills` and `handleSkillsChanged` (App.tsx): a slow read for the
  workspace the user left no longer overwrites the current catalog. The
  registry-ready gate is intentionally skipped on a discarded response — it is
  not an answer about the current identity, and the newer read marks it.
- `loadWorkspaceDirectory` (EntryNavRail.tsx): an in-flight account-A read that
  lands after the rail moved to account B no longer repopulates either the
  module cache or the visible list. This was the sharper of the two, because it
  wrote into module scope and so persisted past the render that caused it.

Request counts are unchanged (startup 4, switch 1): the guard discards
responses, it does not issue requests.

* fix(web): make computing a workspace identity total, and key the skills trigger on it

Fixes the `Web workspace tests` failure 886f11b24 introduced. CI reported all
555 files and 5583 tests PASSING and still exited 1:

  Unhandled Rejection
  TypeError: Cannot read properties of undefined (reading 'canShareProjects')
   ❯ workspaceIdentityCacheKey src/collab/useWorkspaceContext.ts:112:32
   ❯ beginWorkspaceScopedRead src/collab/useWorkspaceContext.ts:159:20
   ❯ src/App.tsx:1770  ❯ src/App.tsx:1799  ❯ commitHookEffectListMount
  originating in tests/components/App.amr-plan-tier.test.tsx

`workspaceIdentityCacheKey` dereferenced `context.permissions` unguarded. That
survived while it was only called during render (a throw there fails loudly, in
the right place) and inside `fetchSkills`'s own try/catch. 886f11b24 put it
behind `beginWorkspaceScopedRead` in an async function launched with
`void refreshSkills()`, so a partial context turned a synchronous programming
error into an unhandled rejection — invisible to every assertion, fatal to the
process.

`permissions` is required on the contract, but API JSON reaches these objects
through an unchecked cast, so a partial context is reachable at runtime and not
only in fixtures. Computing an identity must therefore be TOTAL: a partial
context is its own cache partition, never an exception.

Second, independent defect, found while fixing the first: the skills trigger was
keyed on `workspaceId` while the commit guard compares the full identity digest.
A guard that discards without guaranteeing a successor is worse than the
staleness it replaced — two accounts in one shared team workspace differ only by
membership, so account A's pending read would be discarded for B while the
unchanged workspace id suppressed B's replacement read, leaving the functional
registry loading forever. The trigger now uses the same digest the guard
compares, so every transition that invalidates a response also starts its
successor.

Both are covered by tests that go red without the fix:
- tests/collab/workspace-identity-key-is-total.test.ts (new) — identity is total
  for null, partial and complete contexts, including a late commit-time guard.
- tests/components/App.skills-workspace-scope.test.tsx — a member-id-only change
  must start AND complete a successor read.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* feat(landing): publish current pricing snapshot

* fix: durably deliver enterprise leads

* test(web): update ProjectView comment-status suites to the workspace-scoped signature (#6233)

`feat/workspace-team`'s `Web workspace tests` lane is red at the branch tip: 3
files / 4 tests, all comment-status assertions. Not a flake and not attributable
to any open PR — I had to prove twice while landing #6225 that a red lane was
inherited rather than mine, which is the cost a red base imposes on every
downstream PR.

Cause: `1c15574c2 fix(comments): preserve rejected mutations` gave
`patchPreviewCommentStatus` a fifth argument — the acting workspace context — so
the daemon can authorize the mutation (`routes/project/comments.ts`, same
commit). It updated `FileViewer.test.tsx` and added `registry.test.ts` coverage
but left three `ProjectView.*` suites on the four-argument signature.

The tests were stale, not the code. Checked before editing, because "update the
tests" is the wrong move if the assertions were right:

- Every asserted call still happens, unchanged. The failures are pure arity
  mismatches: `api-empty-response` expects `(…, 'failed')` and receives
  `(…, 'failed', null)`; likewise `needs_review` in run-cleanup and `applying`
  in run-isolation. No status transition changed.
- `1c15574c2` shipped the new contract WITH tests —
  `describe('preview comment scoped mutations')` asserting `x-od-workspace-id`
  reaches the PATCH. The fifth argument is deliberate and covered.
- The parameter is optional and additive; headers are spread only when a
  context is present.

Also restores two assertions that had gone silently vacuous. A four-argument
matcher cannot match a five-argument call, so `run-isolation`'s two
`not.toHaveBeenCalledWith` assertions could never match once the signature grew
and passed unconditionally — coverage actively lost, not merely failing. Both
pass with the correct arity, which independently confirms the new code does not
write `needs_review` after a canceled+done run and does not reset a queued
send's own comment to `open`.

The fifth argument is matched as an explicit `null` rather than
`expect.anything()`, which rejects null/undefined and would have re-broken the
positive assertions and re-vacuated the negative ones.

Tests only — no source file is touched.

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix(collab): make the local pin the only workspace-selection truth (#6230)

* test(collab): pin the one-account-two-clients workspace scope behaviour

Executable evidence for the account-level Active Workspace investigation.
Models vela exactly as its source behaves at origin/main 2155a969f:

- active_workspace_selections is keyed by app_user_id alone
  (db/schema/public.hcl:792), so an account has exactly ONE selection.
- GET /api/v1/workspaces/current discards a ?workspaceId= hint
  (services/api/src/workspaces/routes.ts:166-176, and vela asserts it by
  name in services/api/test/workspaces-personal.test.ts:480).
- GET /api/v1/workspaces is scoped by app_user_id, not by the selection.

What it establishes: OD's local pin DOES hold against a competing client
(resolvePinnedWorkspace), so the switch does not 404. The damage is that
the losing client's context degrades to a directory synthesis - planId
null and a 0/0 seat summary, which derives isSeatFull: true - and its
context read gains a second, fallible round-trip the winning client
never makes.

No behaviour change.

* fix(collab): make the local pin the only workspace-selection truth

Picking a workspace is a local action, but `PUT /api/workspace/active`
gated it on mutating ACCOUNT-scoped state in vela: it PUT the account-level
active workspace first and failed the user's click with 502
`workspace_switch_rejected` when that write did not take, then rolled back
both the local pin and the backend switch if the follow-up context read
disagreed.

That backend row is keyed by app user alone, so it can only ever name ONE
workspace for an account. One account routinely runs several clients in
different workspaces, so every switch yanked the other clients' server-side
scope, and whichever client did not write the row last was answered for the
wrong workspace — forcing OD to synthesize its context from the membership
directory, which carries no billing plane. That client saw planId null and a
0/0 seat summary, and a 0/0 summary derives isSeatFull: true, so a workspace
with free seats read as FULL and blocked inviting. It also turned that
client's context read into two round-trips, the second of which could blip
and leave it with no context at all.

vela #1179 (1ba3eebcb, deployed to test 09:57Z) taught
`GET /api/v1/workspaces/current` to honour `x-vela-workspace-id`, the
per-request workspace scope vela already accepted on its resource plane,
billing scope routes and the Link gateway. So:

- `fetchCurrent` sends that header and drops the `?workspaceId=` query
  param, which vela ignores by design and asserts it ignores — it only ever
  made this call look scoped.
- `PUT /api/workspace/active` no longer calls vela. The membership
  directory it already reads is the authorization; once that confirms an
  active membership the switch cannot fail, so the 502 gate and the
  rollback are gone along with `selectWorkspace` and `putCurrentWorkspace`.
- An unconfirmed context read is no longer treated as a refusal: the route
  answers from the directory entry it already validated instead of
  reverting the user's choice, mirroring the invariant
  `resolvePinnedWorkspace` already follows in the same module.

Verified against the deployed test backend, GETs only: an explicit
workspace returns that workspace (not the account row), a workspace the
caller is not a member of is refused 404 `workspace_member_required`, and
both an absent and a blank header keep the previous account-level
behaviour. The URL query hint is still ignored.

vela's `active_workspace_selections` table and its write path are left
alone; deprecating it is a separate decision and #1179 kept omitting the
header backward compatible.

Two comments that asserted the now-false invariant are corrected.

* fix(collab): never answer a switch with a workspace the pin no longer holds

Review catch (mrcfps): `workspaceContext.current()` is not a passive read.
The vela provider's `resolvePinnedWorkspace` does its OWN fresh directory
lookup and, when that confirms the pinned workspace is gone, clears the pin
and re-pins a recovery workspace so a removed member is not locked out of
everything. The route's directory read can meanwhile have been served from
its 5s cache and still list that workspace.

The previous branch state read `context.workspaceId !== workspaceId` and
treated the provider's recovery as a merely-unconfirmed read, synthesized the
stale cached entry, and returned 200 claiming the requested workspace while
`activeWorkspace.get()` had already become the fallback. That shipped this
PR's own thesis — the local pin is the only selection truth — as a violable
invariant, and the user-visible symptom is the switch appearing to succeed
and then snapping back on the next context poll.

The pin is now re-read AFTER `current()` resolves. If the provider moved it,
the route reports `404 workspace_no_longer_available` for the workspace the
user asked for, never claims it, and does not warm its caches. The recovery
pin is deliberately left in place: reverting it would restore the very
"signed out of every workspace" state it exists to prevent.

The directory entry that authorizes the switch is also now required to be a
LIVE membership (`memberStatus === 'active'`, lifecycle not deleted) — the
same predicate the provider uses. Matching on id alone let a listed-but-
removed membership authorize a switch AND get synthesized into the response.

Tests wire the real provider against the production cached directory fetcher
over one shared pin store, exactly as server.ts does, so the cached and fresh
reads genuinely disagree. Disabling the new pin re-read turns "never claims a
workspace the pin no longer holds" red and nothing else.

---------

Co-authored-by: lefarcen <ontf116@gmail.com>

* fix landing pricing controls

* fix(daemon): resolve the AMR scope-telemetry app version in its own scope (#6240)

Every AMR run in 0.16.2-beta.148 died at spawn with
`spawn failed: appVersionForCapture is not defined`.

`appVersionForCapture` is a local of
`createFinalizedMessageTelemetryReporter`. #6221's
`amr_workspace_scope_resolved` capture called it from `startChatRun`,
~7,500 lines and one function away, so the name resolved to nothing.
The callback runs while building the agent's launch env, inside the
spawn `try`, so the ReferenceError surfaced as the generic
AGENT_EXECUTION_FAILED spawn failure for 100% of AMR runs.

`design.getAppVersion` is the accessor that scope actually has, and it
is the same one the unreachable helper falls back to, so the reported
value is unchanged.

A scope guard covers the class rather than the instance: it runs the
checker over `server.ts` with `@ts-nocheck` stripped and fails on any
identifier that reaches runtime unbound. It flagged one sibling, fixed
here too — the critique orchestrator's `skill` label read
`effectiveSkillId`, a local of `composeDaemonSystemPrompt`. A `typeof`
guard on an undeclared name does not throw, so instead of crashing that
label has silently been `undefined` for every run since #1485.

`@ts-nocheck` on `server.ts` is the enabling condition for both: an
undefined identifier in this file reaches users without any tool
objecting. Removing it is a separate project — 29 daemon files carry it.

* fix(web): isolate workspace identity snapshots

* test(e2e): move the server.ts identifier-scope guard into the merge gate (#6244)

`apps/daemon/tests/server-identifier-scope.test.ts` landed with the
0.16.2-beta.148 AMR spawn fix (#6240) but protected nothing: `ci.yml`'s
"Daemon workspace tests" step runs exactly one file
(`tests/project-watchers.test.ts`), so no other daemon suite executes in
any workflow. The guard sat outside every merge-gate lane.

Widening the daemon lane is not available as a local fix — that exact
command string is pinned by `workflowRunsOnlyAllowedDaemonTest` in
`scripts/check-certain-exempt-consumption.ts`, which conditions the
`trae-cli.test.ts` docs-consumption exception on the lane running nothing
else. Touching it is a coupled scope-policy change.

So host the guard where a lane already runs and the wiring is itself
guarded:

- Any change under `apps/daemon/src/` matches `certain-daemon-core`
  (`scripts/scopes.ts`), whose effects include
  `ui_p0_validation_required`; `run_e2e_vitest` is
  `isFull || web_tests_required || ui_p0_validation_required`. A
  `server.ts`-only change therefore arms `E2E Vitest` even at the merge
  queue's `certain` threshold, and an unresolved file list escalates
  fail-closed to full.
- The `daemon core boundary` guard (`scripts/lib/guard/scope.ts`) asserts
  `ci.yml` still contains both `run_e2e_vitest == 'true'` and
  `pnpm --filter @open-design/e2e test`, and runs in the always-on policy
  floor — so the premise cannot rot silently.

The check only ever reads `server.ts` as text (no daemon boot, no
fixtures), and reading another app's tree as a repository resource is
what root `AGENTS.md` sends to `e2e/tests/` — the same reason
`e2e/tests/critique-coverage.test.ts` lives there. Behavior is
unchanged, including both self-checks that keep the guard from going
vacuous.

Verified red on the pre-#6240 `server.ts`: exit 1 naming
`appVersionForCapture (line 9261)` and `effectiveSkillId (lines
9657/9658)`; green (exit 0) on the fixed file.

* fix(collab): align unbound project mutation identity (#6231)

* fix(web): name the caller when a run starts before its project scope resolves

Sending the 「水面焦散」 example prompt from Home failed instantly on a team
workspace:

    daemon 401: {"error":{"code":"WORKSPACE_CONTEXT_REQUIRED","message":"workspace context is required"}}

with `run_id: n/a`, because the refusal lands before the run is created. That
string is produced in exactly one place in the repo — the `!createResp.ok`
branch of the `POST /api/runs` create fetch in `providers/daemon.ts` — so the
refused call is run creation, gated by `enforceWorkspaceResourceMutation
('project', …)`.

The cause is client-side. Home creates the project WITH the caller's workspace
headers, so #6201 binds it to the team workspace; the follow-on auto-send then
went out with NO headers, because it took its identity from the PROJECT's
workspace scope — an async read still in flight when the auto-send fires (its
gate is `messagesInitialized` + `activeConversationId`, never the scope). Every
other project write in `ProjectView` (`patchProject`, `uploadProjectFiles`, the
comment mutations) already asserts the caller's own context; run creation was
the one write that did not.

INVARIANT, now named in `runWorkspaceIdentity`: a send names its caller while
the project's binding is still UNREAD, and never overrides an answer the daemon
actually gave. Deliberately not a blanket `?? caller` — every answered or failed
state keeps asserting nothing, because for them a headerless request is judged
on daemon-owned state while an asserted one is judged on client-controlled
headers:

  - `unbound` — `headerlessMutationAllowed` short-circuits on "no row anywhere"
    BEFORE asking for an identity, so asserting would turn a working 200 into a
    403 and break 「未登录也可以用自己 cli 修改未登录态下的那些 project」.
  - `unavailable` — the daemon's `resolveProjectWorkspaceScope` returns this
    both for an unreadable directory AND for a directory that answered with no
    active membership, i.e. a proven "you were removed".
  - a FAILED scope read — this identity also picks the wallet the AMR pre-run
    balance gate prices against, and guessing the caller's team wallet with the
    binding unknown blocks a send the daemon would have answered (the dead
    button 21f452ffe undid).

Second consequence of the same null, fixed by the same one-liner: with no
resolved scope, `checkAmrBalanceGate` was called with `undefined` and priced the
run against the ACCOUNT wallet instead of the team's. The red spec observes
that directly rather than arguing it.

`ProjectView.run-isolation.test.tsx` mocks `useProjectWorkspaceScope` with a
hand-written factory that enumerates every export, so the new export is added
there too — without it, adding one export breaks all 64 tests in that file with
"not a function" at render.

* fix(web): narrow the run-identity fallback to a project's first unread binding

Review catch (nettee): keying the fallback on `loading` served two opposite
situations, because `useProjectWorkspaceScope`'s trailing transition guard
re-enters `{ loading: true, scope: null }` whenever the resolved caller identity
stops matching — and every workspace switch does that:

  - first read, nothing known    -> the caller is the only identity available
  - revalidating a KNOWN binding -> the caller's current workspace is a guess
    that contradicts an answer we already have

So a send issued during a workspace switch asserted the workspace the user had
just moved TO for a project still bound to the one they moved FROM. Both
consumers break, and differently: `POST /api/runs` can 403 because
`enforceWorkspaceResourceMutation` still finds the project row under the old
workspace, and `checkAmrBalanceGate` preflights the new workspace's wallet —
worse than the `undefined` this fix was written to remove, because a wrong
wallet looks like a real answer.

`ProjectWorkspaceScopeState` now carries `initialLoadPending`, reported by the
hook rather than inferred from `loading`: true from mount until the first settled
answer (a scope OR a failure) for the current `projectId`, false for that
project's whole lifetime afterwards, and reset when `projectId` changes because a
different project genuinely has not been read. `runWorkspaceIdentity` keys the
fallback on that bit alone, and its docblock names which of the two loading
reasons it serves so the distinction cannot erode.

The field is required, not optional, so every construction site has to state
which case it is. Two pre-existing assertions in
`useProjectWorkspaceScope.test.tsx` now document the distinction they always
exercised: a project A->B rerender reports pending (B is unread), while a
same-project identity change reports not-pending — the exact window this fixes.

* fix(collab): align unbound project mutation identity

Allow an identified caller to mutate a project that no workspace has claimed, matching the existing headerless and unbound-resource behavior without persisting a new binding. Name the caller on Home auto-send while keeping AMR preflight scope derivation unchanged.

* fix(web): refresh the settled run billing scope

Track the project billing context in both callbacks that read it so an auto-send after scope settlement cannot preflight through a stale unscoped closure. Add a regression whose run identity stays referentially stable while the billing scope changes.

* fix(web): stop asserting disproved run scope

* fix(web): guard workspace-scoped catalog reads (#6249)

* fix(web): guard workspace-scoped catalog reads

* fix(web): guard remaining plugin catalog races

* fix(web): align design catalog guard scope

* fix(landing): show introductory team totals

* fix(daemon): label critique with project skill (#6251)

* fix CI scopes across file renames (#6252)

* fix(ci): preserve scopes across renames

* fix merge queue rename ceiling detection

* test raw merge compare responses

* fix(landing): make contact sales KV canonical

* fix(web): preserve Cloud sign-in during outages (#6274)

* fix(collab): allow shared project viewer comments

* fix(web): keep readonly comment creation available

* feat(web): personal/team project labels + collapse chat for shared non-owners (#6294)

* feat(web): rename sidebar project lists and collapse chat for shared non-owners

Workspace home left rail now labels personal vs team project lists
("Personal projects" / "Team projects", 个人项目 / 团队项目) across all
locales. Opening a confirmed shared team project that the viewer did not
create defaults the chat pane to collapsed so the design is primary;
owners and personal projects keep chat open, and expanding chat stays
sticky for the visit.

* fix(web): use effective ownership for shared non-owner chat default

Default-collapsed chat was latching on raw isOwner, so a catalog-confirmed
owner whose collab status was still missing ownerMemberId looked like a
non-owner and permanently collapsed chat. Expose isEffectiveOwner and
isSharedNonOwner from useProjectCollab (catalog + session-created + status)
and gate the sticky collapse path on the confirmed non-owner signal only.

* fix(web): stop narrow chat error cards stacking titles vertically (#6295)

Move the long Open Design Cloud switch CTA into UserActionCard footer
actions (same shell as run-recovery), add a card-width container query so
head actions reflow when ChatPane is narrow, and soften title wrapping so
CJK copy no longer renders one character per line.

* fix(workspace): make resource authority explicit (#6287)

* fix: make workspace authority explicit across resources

* fix(web): keep new team projects writable during scope load

* fix: close workspace authority review gaps

* fix(web): retry rejected project auto-send

* fix: propagate workspace scope to project consumers

* fix: cache project read workspace authority

* fix(web): stabilize project scope revalidation

* fix: cache project collaboration status authority

* fix: cache collaboration status owner reads

* fix: revalidate media wait tool authority

* fix: rehome member comments on local threads

* fix(web): stabilize project resource authorization

* fix(web): prioritize settled project authority

* fix(web): consume queued home auto-send handoffs

* fix: revalidate dirty comment pulls

* fix: isolate mutation authority reads by session

* fix: cache project presence read hot path

* fix(web): stop unauthorized project background writes

* fix(web): require settled scope for project background work

* fix(web): scope remaining project reads by workspace

* fix(web): scope remaining project resource consumers

* fix(web): partition project caches by full authority

* fix(daemon): pin run workspace billing scope

* fix(daemon): lease workspace read authority

* fix(daemon): keep presence reads on the hot path

* fix(web): refresh deck thumbnails across workspace scope

* fix(daemon): revoke stale unshared team mirrors

* perf(daemon): cache stale mirror read guards

* fix(web): gate deep-link reads on project workspace

* fix(web): keep deep-linked project tab scoped

* fix(web): preserve project routes during onboarding

* fix(web): keep project previews warm across file-list toggles

* perf(daemon): refresh presence without cold reads

* fix(web): keep retained preview iframes connected

* fix(web): reuse exact HTML thumbnail sources

* fix(web): settle read-only empty project chat

* fix(daemon): scope CLI run transports by workspace

* test(web): align preview cache refresh semantics

* fix(web): require positive shared viewer ownership proof

* fix preview auto-fit measurement feedback

* preserve preview fit during annotation freezes

* preserve preview measurement semantics on refresh

* stabilize preview overflow measurement test

* test design systems move exclusively to team

* fix design systems move exclusively to team

* test shared project title scope convergence

* test(daemon): cover project command workspace scope

* fix(daemon): scope project command transports

* fix shared title test project state type

* fix(daemon): preserve shared project catalog title

* fix(daemon): keep foreign mirrors creator-unattributed

* fix(web): recover first shared project conversations

* fix(web): fence conversation recovery generations

* test(web): verify conversation recovery cleanup fence

* UI polish batch: cloud sign-in, onboarding, composer menu (#6281)

* UI polish batch: cloud sign-in, onboarding, composer menu

Sign-in surfaces (rail callout, balance-gate dialog, onboarding) had
drifted from the visual system and lost some redundant chrome; this
batch tightens them up alongside a couple of composer/design-files
reorganizations that were sitting in the same working tree:

- Rail's signed-out callout: reorder copy above the login pill, give
  the pill an icon + accent-green label, restore the Escape hatch
  the pill had before it lost its close affordance.
- AmrBalanceDialog (insufficient-balance / signed-out gate): add a
  full-bleed art banner above the copy, drop the now-redundant icon
  badge, tighten spacing/shadows to match.
- Onboarding cloud landing: switch from a single centered column to
  a two-column layout (form pane + full-bleed art panel), move the
  language switcher into the footer, swap the local-CLI/BYOK links
  for icon Buttons, and add a dismiss control to the activation-retry
  hint.
- Composer "+" menu: fold 插件/设计百宝箱 back in as hover-expand
  submenu rows instead of standalone quick pills above the input.
- Design files panel: move new-document/upload actions out of the
  always-visible toolbar into the empty-state actions only.
- Message center: drop the redundant header close button (backdrop
  click and Escape still dismiss it) and restyle the list from
  bordered cards to a divider-separated list.
- New `key` / `robot` / `translate` / `arrow-right` icons; language
  menu switches from the `languages` glyph to `translate`.
- Tab launcher popover reverts to the glass material; language
  dropdown gets a max-height + scroll so long lists don't blow out
  the popover.

Rebased the composer-quick-pill removal onto this branch's own
accessibility work on those pills (opener-based focus return via
`ComposerStandalonePanel`) rather than reverting it — the "next step"
card still drives the same standalone-panel API, just without a pill
to hand focus back to. Also updated/removed the tests that pinned the
now-removed surfaces (quick-pill popup contract, message-center close
button, onboarding top bar, design-files toolbar upload trigger) so
none of this lands with a newly-red suite.

* perf(web): optimize onboarding cloud artwork

* fix(web): invalidate project file reads after mutations

* fix(web): disable readonly project starter mutations

* fix(web): preserve focus for standalone composer panels

* test(e2e): require explicit workspace scope

* fix(web): invalidate file reads on project events

* test(e2e): seed explicit settings workspace

* test(e2e): align manual edit with current inspector

* fix(web): refresh deployments when share opens

* test(e2e): follow composer design system entry

* fix(web): keep local project creation workspace-independent

* fix(web): preserve scoped auth recovery state

* test(e2e): follow current entry settings flows

* fix(web): preserve AMR retry across settings

* test(e2e): model explicit personal workspace recovery

* test(e2e): stabilize AMR logout settings recovery

* fix(projects): return workspace scope on create

---------

Co-authored-by: lefarcen <935902669@qq.com>

* fix(workspace): preserve scoped projects after main sync (#6320)

* fix(daemon): surface AMR OpenCode stall context (#6040)

* fix(daemon): surface AMR OpenCode stall context

* fix(daemon): accept runtime-omitted OpenCode diagnostics

Generated-By: looper 0.11.0 (runner=fixer, agent=codex)

* fix(ci): make static gate runner-independent (#6039)

---------

Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>

* test(e2e): parallelize restoration group (#6169)

* test(e2e): parallelize restoration group

* test(e2e): identify conversations from persisted prompts

Generated-By: looper 0.11.0 (runner=fixer, agent=codex)

* test(e2e): wait for conversation persistence

---------

Co-authored-by: Looper <looper@noreply.github.com>

* feat(landing-page): sharpen the Codex agent page for "codex ui" intent (#6200)

The /agents/codex-design/ page ranks #1 for "codex design" but only ~6th
with a 1.3% CTR for "codex ui", a query that grew from 30 to ~90 daily
impressions this month. A live SERP check shows the intent is mostly
"build UI with Codex" (our intent), with a minority looking for the Codex
app's own interface.

- Retitle to "Codex Design: Build UI with OpenAI Codex" (title + H1).
- Lead the frontend section with the query phrase and an answer-shaped
  opening paragraph, so the section is extractable for AI answers.
- Add a disambiguation paragraph separating the Codex app's own UI from
  the UI you build with Codex, and route the former back to the main line.
- Add two FAQ entries covering both readings; they also feed the FAQPage
  schema, which already earns a text snippet on this query.

Applied across all 11 shipped locales (en, zh authored; the other nine
translated with the page's terminology preserved).

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>

* Add Clone Audit plugin (#5687)

* Add Clone Audit plugin

* Update open-design.json

* fix: make clone audit skill reusable

* fix: sanitize clone audit provenance path

* fix(e2e): bound tools-dev runtime lifecycle (#6204)

* test(e2e): refresh full-pool UI fixtures (#6211)

* fix(web): allow skipping required chat questions (#6177)

* fix(web): allow skipping required chat questions

* fix(web): auto-skip unanswered chat questions

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>

* docs(tools-pack): add build cache contract (#6207)

* docs(tools-pack): add build cache contract

Document the tools-pack build-graph cache as a contract: the exact-match
acceptance model, determinant rules, materialization-time parameters, the
signing boundary, and fail-closed confidence grading.

The rules come from an audit of all 11 cache nodes. Two are written down
because nothing currently enforces them: a node key must carry the key of
every upstream node it consumes, and a node key must not restate a list that
already exists as a constant.

Known low-confidence points are declared explicitly so fail-closed grading
stays checkable rather than becoming a verbal convention.

* docs(tools-pack): clarify cache materialization exceptions

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* docs(tools-pack): document NSIS base version scope

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>

* docs(landing-page): add 0.16.0 release post, localize and re-cover 0.15.1 (#6236)

* docs(landing-page): localize 0.15.1 post, fix its cover, add 0.16.0 release post

The 0.15.1 blog post shipped English-only and with an off-brand cover: a
Fauvist oil painting overlaid with large display type, which reads nothing
like the warm editorial illustrations every other release post uses. It also
pointed `socialImage` at a 309KB JPEG while a WebP sat unused beside it.

Regenerate the cover in the established house style (cream paper ground,
sage/terracotta watercolour washes, pencil sketch resolving into a calm
product surface), drop the stray JPEG, and add the full ten-locale i18n block
so /zh/, /ja/, /ko/, /de/, /fr/, /ru/, /es/, /pt-br/, /it/ and /tr/ read in
their own language instead of falling back to English.

Also add the missing 0.16.0 "Reliable Delivery" post with a cover plus two
inline figures in the same style, localized across the same ten locales. The
0.16.1 patch is a single preview fix, too thin to carry its own page, so it
lands as a closing section of the 0.16.0 post and the download CTA points
there.

* docs(landing-page): fix doubled apostrophes and locale copy in release posts

Three defects surfaced reviewing the localized bodies.

The French, Italian and Turkish bodies rendered every apostrophe twice —
"aujourd''hui", "all''interno", "0.16.1''i". The `''` escape belongs to
single-quoted YAML scalars, but `bodyHtml` is a block scalar, where content is
literal. 173 occurrences across the two posts.

Korean used Japanese corner brackets and Brazilian Portuguese and Turkish used
French guillemets. All three take curly double quotes.

The Chinese copy read as translation rather than prose. "标签" opens the release
line as a UI label, not a version tag; "抢画框", "编辑策展位" and "安静修复" are
literal renderings that say nothing in Chinese; "画廊" is not the term the
product itself uses for a gallery, which keeps the English word. The tag fix
also lands on 0.10.0 through 0.15.0, which share the phrasing — leaving the
series half-corrected would read worse than either state alone.

* docs(landing-page): drop the leading tag word from release post openings

Every release post opened with the word for a git tag before the version
identifier — "Tag open-design-v0.16.0", "Etiqueta …", "タグ …", and in Chinese
"标签", which reads as a UI label rather than a version. The word carries no
information the identifier does not already carry, and in several languages it
lands as a category noun the sentence never uses again.

Drop it across all nine release posts and every locale, in both the summary and
the opening line, leaving the version identifier to open the dateline. Spanish
and Brazilian Portuguese lose the feminine agreement that referred back to the
removed noun; Turkish loses the trailing "etiketi" apposition for the same
reason.

* docs(landing-page): correct the 0.16.1 interval in the 0.16.0 post

0.16.0 was published 2026-07-22 14:42 UTC and 0.16.1 on 2026-07-23 16:11 UTC —
25.5 hours apart. "Two days later" overstated it, and the same wording had been
carried into all ten localized bodies, so every published route repeated it.

---------

Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>

* feat(integrations): add the local Open Design Cloud flow for Codex (#6055)

* feat(daemon): expose Vela login through CLI and MCP

* feat(mcp): add interactive local brief card

* feat(packaged): bootstrap local MCP headlessly

* feat(daemon): secure local BYOK credential profiles

* feat(cli): manage secure BYOK profiles

* feat(web): store BYOK keys in secure profiles

* fix(runs): deduplicate cloud generation requests

* feat(daemon): add end-to-end plugin observability

* fix(pack): include native PTY runtime in desktop packages

Lazy-load node-pty in the daemon so missing native binaries only
disable Terminal and interactive Keychain operations instead of
crashing startup.

Ship and validate target prebuilds in macOS and Windows packages.
Repair the macOS spawn helper before signing, and invalidate stale
Windows packaging caches.

Cover the packaged PTY path in macOS and Windows smoke tests. Require
Vela CLI for every beta desktop build target.

* fix(mcp): localize and stabilize plugin briefs

Keep collect and confirm on one MCP Apps card, report intrinsic size changes,
and remove the fragile widget-state fallback.

Localize brief forms, summaries, status, and public errors from the current
request or Host locale while preserving stable protocol ids and selections.

Keep internal runtime identifiers out of user-facing MCP guidance.

* fix(packaged): register live Studio URLs with daemon

Register the packaged web sidecar loopback origin over namespace-scoped daemon
IPC after dynamic binding.

Invalidate install-info by the live web port and remove the second MCP URL cache
so run and project responses use the current Studio address.

Validate loopback origins and cover registration, rebind, and cache refresh.

* fix(mcp): stabilize brief confirmation lifecycle

Keep confirmed briefs locked after Host publication failures and retry only the cached follow-up message instead of confirming twice.

Replace blank initialization with a compact loading state, accept immediate and delayed tool results, and bump the MCP Apps resource to v3 so Codex does not reuse the stale card.

Add an executable JSDOM Host bridge harness covering double-submit, publication retry, delayed results, and repeated draft notifications. Validated with the full daemon suite, workspace typecheck, and guard.

* fix(mcp): prevent brief app resize feedback loops

Replace the self-observed intrinsic-height loop with deduplicated body measurements so Codex MCP app iframes do not repeatedly resize themselves and fail sandbox startup.

Bump the brief resource URI to v4 to avoid cached v3 cards and cover the regression with the focused brief-app spec. Validated with daemon tests/build, workspace guard/typecheck, a verified beta DMG, and a direct packaged MCP resource smoke.

* fix(mcp): use the final Open Design plugin identity

Make open-design the only accepted external plugin ID across MCP tools, run attribution, artifact version origins, and telemetry. Rename the Brief MCP App resource to v5 and reject the unpublished old ID instead of aliasing it.

Validated with 171 daemon tests, 5 web tests, daemon/web typechecks, and guard.

* chore(pack): bump Vela CLI to 0.0.27

* chore(nix): refresh pnpm dependency hashes

* fix(mcp): honor host and plugin request contracts

Respect independently advertised MCP Apps capabilities and prefer the Codex-native follow-up bridge so confirmed briefs render as readable user turns instead of context-only messages. Bump the brief resource to v6 to invalidate stale cards.\n\nMark Vela login and account probes as open-world interactions, and require stable request IDs for attributed generation so lost responses cannot create a second logical run.

* fix(cli): support keyless BYOK profiles

Add an explicit --no-api-key save mode for local and self-hosted providers while keeping credential-bearing profiles stdin-only. Reject missing or conflicting credential modes and cover both paths in the CLI tests.

* fix(pack): replace headless owner when desktop opens

Advertise headless packaged runtimes as having no visible window, then shut them down before a normal desktop launch continues instead of treating SHOW as a successful focus.

Validated with the focused packaged headless and launcher lifecycle tests (20 passed).

* fix(pack): clean up failed headless startup

Acquire the headless identity, sidecars, and IPC server through one failure-atomic lifecycle so later bootstrap errors close every previously acquired owner in reverse order.

Cover MCP-install and web-identity publication failures; validated with 22 focused tests and the packaged typecheck.

* fix(byok): remove raw provider request contract

Expose only daemon-owned BYOK profile references from shared run types and the web transport, while retaining the daemon's fail-closed rejection for legacy or untyped raw credential payloads.

Validated with 76 web provider tests plus web and contracts typechecks.

* fix(daemon): preserve interrupted run idempotency

Hydrate queued or running durable requests as DAEMON_RESTARTED failures before idempotency reuse, preserving the original request and workflow mappings instead of creating a second cloud operation.

Share the restart transition with boot reconciliation and expose synthetic terminal events to reattaching clients. Validated with 48 focused tests and the daemon typecheck.

* fix(byok): roll back failed credential metadata writes

Snapshot secure-store values before mutation and compensate create, update, and delete operations when profile metadata cannot be persisted, preventing orphaned or lost credentials.

Validated with nine focused credential-service tests and the daemon typecheck.

* fix(web): reconcile selected BYOK profile metadata

Treat the daemon's selected secure profile as authoritative for protocol, endpoint, model, and API version so CLI profile edits cannot leave the browser showing stale execution settings.

Validated with 66 config tests and the web typecheck.

* fix(byok): migrate legacy credentials before cleanup

Import each supported legacy browser credential into a stable daemon-owned secure profile before removing plaintext copies. Keep the original browser record on secure-store or unsupported-provider failures and surface a Settings recovery path.

Cover successful migration, duplicate projections, and injected secure-backend failure while preserving existing Bedrock downgrade behavior in memory.

* fix(byok): secure Windows profiles with DPAPI

Dispatch native Windows BYOK profiles to a CurrentUser DPAPI backend rooted under the resolved daemon data directory. Secrets enter PowerShell only through stdin, encrypted blobs stay separate from non-secret profile metadata, and API responses remain secret-free.

Add platform dispatch coverage, a Windows-only real DPAPI round-trip test, and the packaged Windows smoke path for create, resolve, response-redaction, and delete behavior.

* fix(web): preserve config writes during hydration

* fix(byok): persist secure credentials across entry flows

* test(byok): cover secure onboarding persistence

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>

* test: strengthen E2E coverage and fixtures (#6150)

* test: strengthen e2e coverage and fixtures

* test: allow artifact route when starting conversation

* test: keep e2e count assertions stable

Use the shared stable-count helper for no-request and duplicate-event windows so delayed async events cannot escape the assertions.

Generated-By: looper 0.11.0 (runner=fixer, agent=codex)

* fix: add e2e assertion import extension

Use the explicit runtime extension required by the e2e NodeNext typecheck.\n\nGenerated-By: looper 0.11.0 (runner=fixer, agent=codex)

* fix: preserve design system provenance and settle e2e assertions

Keep disabled design systems available to project recovery and provenance while filtering only runtime injection. Make hover and cancellation checks observe their full settled windows.

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* test(e2e): preserve unique restoration run IDs

Pass the restoration fixture's run ID prefix through routeSuccessfulRuns so each mocked request receives a distinct run identity.

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* test(e2e): stabilize agent and async count fixtures

Route restoration agent requests through the shared JSON and SSE fixture, capture autosave baselines after a quiet period, and observe onboarding cancellation for a bounded stable window.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix: honor disabled design systems in daemon runs

Filter disabled persisted project selections at prompt and analytics boundaries, and keep restoration run fixtures unique.\n\nGenerated-By: looper 0.11.2 (runner=fixer, agent=codex)

* test(e2e): shorten cancellation duplicate check

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(daemon): continue stalled post-tool sessions (#6237)

* fix(daemon): continue stalled post-tool sessions

* fix(daemon): separate post-tool continuation budget

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(daemon): preserve retry telemetry budget

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(daemon): align retry failure telemetry

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(tools-pack): carry workspace build key into tarballs (#6235)

* fix(amr): recover late login failures and trace auth stages (#5986)

* fix(amr): recover late login failures and trace auth stages

* fix(amr): preserve scoped cancel attempt IDs

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): defer unsupported Vela auth stages

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): narrow fallback telemetry to shipped Vela

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): preserve live login after stale cancel

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): preserve cancel during login startup

Retain cancellation intent when a provisional cancel races the delayed canonical login response, then cancel the canonical attempt before polling can begin.

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): rejoin newer login after startup cancel

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): preserve cancel across web login starts

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): retain cancel when status refresh fails

Generated-By: looper 0.11.1 (runner=fixer, agent=codex)

* fix(amr): cancel onboarding status preflight

Treat cancellation before an auth attempt exists as a local preflight cancel so a delayed status response cannot start login.\n\nGenerated-By: looper 0.11.1 (runner=fixer, agent=codex)

* ci: constrain Actions cache writes (#6270)

* fix(tools-pack): carry workspace build key into tarballs

* ci: constrain Actions cache writes

* fix(daemon): auto-mint assistantMessageId when clients omit pin (#6107)

* fix(daemon): auto-mint assistantMessageId when API clients omit it

Web always supplies assistantMessageId. Headless/MCP/eval clients that
already send conversationId but omit the pin left lastMessageId null and
skipped multi-turn native session resume (missing_cursor). Mint the id
server-side whenever a conversation is bound and the client did not, and
seed the user message the same way the old MCP-only path did.

* fix(daemon): validate conversation ownership before omit-pin seed

Reorder project-ownership checks before minting assistantMessageId and
seeding the user prompt so cross-project omit-pin requests cannot corrupt
foreign chats. Make ChatRunCreateRequest.assistantMessageId optional and
expose conversationId/assistantMessageId on McpRunCreateRequest with
contract tests for the omit-pin shape.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): reject missing conversation before omit-pin mint

Return CONVERSATION_NOT_FOUND when a client supplies a stale or mistyped
conversationId so omit-pin does not mint an unpersisted assistantMessageId
and plugin snapshot resolve does not FK-crash with a 500.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed latest turn and fallback-bound user prompts

Prefer currentPrompt over full message transcripts when seeding omit-pin
user turns, and still seed after conversationId fallback when the client
already supplied assistantMessageId.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): preserve attachments on omit-pin seeded user turns

Copy request attachments and commentAttachments into the server-seeded
user message so omit-pin/headless clients keep chips and annotation
context after conversation reload.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): treat empty currentPrompt as authoritative omit-pin seed

Attachments-only ChatRequest turns send currentPrompt as '' while message
is the flattened transcript. Prefer any string currentPrompt, including
empty, so seeded user rows keep empty content and do not duplicate history.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed turn metadata and bump project activity on omit-pin

Persist sessionMode, runContext, and appliedPluginSnapshot on omit-pin
seeded user turns so reload/retry keep applied context. Call updateProject
after seed so listProjects reorders headless/API activity like PUT /messages.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): require projectId for bound conversation ownership

Reject POST /api/runs when conversationId is present but projectId is
missing or non-string, so omit-pin cannot seed messages without owning
project context. Cover both early and post-fallback ownership checks.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): preserve slideIndex and BMP kind on omit-pin seeds

Keep deck annotation slideIndex and classify .bmp as image when seeding
user messages for clients that omit assistantMessageId.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed empty message when omit-pin has attachments

When currentPrompt is unset and message is empty, still seed the user
turn if attachment metadata is present so chips/annotations survive reload.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(daemon): seed omit-pin chat content from original request

Plugin resolution may rewrite meta.message with a rendered scenario
brief for the run; persist requestBody currentPrompt/message for the
user turn so the internal brief is not shown as chat content.

Generated-By: looper 0.11.1 (runner=fixer, agent=grok-build)

* fix(web): persist browser preview viewport (#4899)

* fix(daemon): honor Kiro ACP turn completion (#6268)

* fix(daemon): isolate plugin-started Local Codex runs (#6273)

Disable Codex plugins only for externally attributed Open Design Plugin runs so Local Codex cannot recursively enter collect_brief or Cloud login. Preserve normal Local Codex plugin loading and the existing operator-wide override.

Validated with the focused runtime args suite, workspace typecheck, guard, and an isolated source smoke that produced a valid artifact without Cloud login.

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>

* fix(landing): accept signed Vela team plan leads (#6283)

* fix: durably deliver enterprise leads

* fix(landing): make contact sales KV canonical

* feat(prompts): introduce on-demand discovery in SP v2.0 (#6223)

* feat(prompts): introduce on-demand discovery in SP v2.0

* fix(prompts): prevent nested deck label overlap

* fix(plugins): stabilize od-default task routing

* fix: address prompt and deck audit regressions

* fix(prompts): restore plan mode precedence

* fix(daemon): accept all form answer headers

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>

* fix(web): surface BYOK migration validation errors (#6280)

* fix(web): surface BYOK migration validation errors

Preserve structured daemon errors during legacy credential migration and reserve offline guidance for fetch failures.

Keep legacy browser credentials until migration succeeds, and cover HTTP validation, malformed responses, and network failures.

Validated with focused web tests, web typecheck, workspace guard, and workspace typecheck.

* fix(web): stabilize BYOK persistence error telemetry

Map secure-profile HTTP and network failures to semantic error codes before emitting settings_byok_test_result, while preserving existing fallback behavior for unrelated connection-test errors.

Validated with the complete web test suite, web typecheck, workspace typecheck, guard, and git diff --check.

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>

* fix(ui): improve dark-mode contrast for off-state toggles (#5825)

* fix(ui): improve dark-mode contrast for off-state toggles

The default off-state track uses --border-strong (#c9d0da light / #46433c dark).
In dark mode #46433c is only ~5% lighter than the --bg-subtle (#252321) panel
behind it, making the toggle nearly invisible when off and users unsure of the
control state.

Bump the off-state track to #6b6862 with a subtle inset border, and add a
thin outline to the white thumb so it stays visible against the (now lighter)
track. On-state already uses --text (#e8e4dc) and is left unchanged.

Applies to both .toggle-row (used by speaker-notes toggle in the deck tab)
and .compact-toggle (used by SurfaceOptions toggles). Hover lift gives an
additional interactive affordance.

Fixes #5298

* fix(ui): fix two dark-mode toggle blocking issues from review

Two correctness issues called out by mrcfps:

1. System dark theme never gets the contrast fix — the default "system"
   theme mode removes [data-theme] and falls back to
   @media prefers-color-scheme. Added a parallel block scoped to
   html:not([data-theme]) inside @media (prefers-color-scheme: dark).

2. Hover override wins over on-state — .compact-toggle:hover and
   .toggle-row:hover have higher specificity than .on
   variants, so hovering an ON toggle dropped its track from bright
   var(--text) to #75726c. Changed to :not(.on):hover so the hover
   lift only applies when off, leaving the on-state always bright.

Both applied to both selector paths (explicit data-theme and system dark).

* fix(web): keep Azure deployment names editable (#6034)

* chore(plugin-previews): refresh baked preview manifest (#6293)

Co-authored-by: open-design-bot <bot@open-design.ai>

* fix(byok): withdraw Windows DPAPI backend (#6308)

* fix(byok): withdraw Windows DPAPI backend

* fix(byok): clean up retired Windows secret blobs

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>

* fix(web): dismiss stale todo after continuation starts (#6307)

* fix(web): preserve shared project update timestamps

* fix(web): reconcile shared timestamps on home

* fix(web): preserve workspace scope in preview assets

* fix(collab): avoid unscoped shared cover requests

* fix(web): avoid double-wrapping preview assets

* fix(web): preserve first prompt across project refresh

* test(web): target the active shared project card

* test(web): keep empty BYOK profile type-safe

* test(e2e): require explicit workspace scope

* fix(web): finish scoped preview hydration

* test(e2e): align manual edit with current inspector

* fix(web): reconcile files after event stream readiness

* fix(web): keep local project creation workspace-independent

* fix(web): preserve scoped auth recovery state

* test(e2e): assert explicit Personal workspace scope

* test(e2e): preserve visual AMR account fixture

* test(web): await scoped cover source

* UI polish batch: cloud sign-in, onboarding, composer menu (#6281)

* UI polish batch: cloud sign-in, onboarding, composer menu

Sign-in surfaces (rail callout, balance-gate dialog, onboarding) had
drifted from the visual system and lost some redundant chrome; this
batch tightens them up alongside a couple of composer/design-files
reorganizations that were sitting in the same working tree:

- Rail's signed-out callout: reorder copy above the login pill, give
  the pill an icon + accent-green label, restore the Escape hatch
  the pill had before it lost its close affordance.
- AmrBalanceDialog (insufficient-balance / signed-out gate): add a
  full-bleed art banner above the copy, drop the now-redundant icon
  badge, tighten spacing/shadows to match.
- Onboarding cloud landing: switch from a single centered column to
  a two-column layout (form pane + full-bleed art panel), move the
  language switcher into the footer, swap the local-CLI/BYOK links
  for icon Buttons, and add a dismiss control to the activation-retry
  hint.
- Composer "+" menu: fold 插件/设计百宝箱 back in as hover-expand
  submenu rows instead of standalone quick pills above the input.
- Design files panel: move new-document/upload actions out of the
  always-visible toolbar into the empty-state actions only.
- Message center: drop the redundant header close button (backdrop
  click and Escape still dismiss it) and restyle the list from
  bordered cards to a divider-separated list.
- New `key` / `robot` / `translate` / `arrow-right` icons; language
  menu switches from the `languages` glyph to `translate`.
- Tab launcher popover reverts to the glass material; language
  dropdown gets a max-height + scroll so long lists don't blow out
  the popover.

Rebased the composer-quick-pill removal onto this branch's own
accessibility work on those pills (opener-based focus return via
`ComposerStandalonePanel`) rather than reverting it — the "next step"
card still drives the same standalone-panel API, just without a pill
to hand focus back to. Also updated/removed the tests that pinned the
now-removed surfaces (quick-pill popup contract, message-center close
button, onboarding top bar, design-files toolbar upload trigger) so
none of this lands with a newly-red suite.

* perf(web): optimize onboarding cloud artwork

* fix(web): invalidate project file reads after mutations

* fix(web): disable readonly project starter mutations

* fix(web): preserve focus for standalone composer panels

* test(e2e): require explicit workspace scope

* fix(web): invalidate file reads on project events

* test(e2e): seed explicit settings workspace

* test(e2e): align manual edit with current inspector

* fix(web): refresh deployments when share opens

* test(e2e): follow composer design system entry

* fix(web): keep local project creation workspace-independent

* fix(web): preserve scoped auth recovery state

* test(e2e): follow current entry settings flows

* fix(web): preserve AMR retry across settings

* test(e2e): model explicit personal workspace recovery

* test(e2e): stabilize AMR logout settings recovery

* fix(projects): return workspace scope on create

---------

Co-authored-by: lefarcen <935902669@qq.com>

* test(e2e): lock design system team partition

* fix(workspace): stabilize scoped project lifecycle

* test(e2e): stabilize retained workspace transitions

* fix(workspace): retain exact scope through transient outages

* test(workspace): lock outage identity boundaries

---------

Co-authored-by: Ray Xi <2667192167@qq.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Looper <looper@noreply.github.com>
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
Co-authored-by: bestthanapon <thanapon.suwannasuk@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: Amy <58060647+AmyShang-alt@users.noreply.github.com>
Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: _HDCoder <2728073932@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Nicholas-Xiong <2482929840@qq.com>
Co-authored-by: mehmet turac <mehmetturac@gmail.com>
Co-authored-by: open-design-release-bot[bot] <295937643+open-design-release-bot[bot]@users.noreply.github.com>
Co-authored-by: open-design-bot <bot@open-design.ai>
Co-authored-by: CHENGLONG WANG <honam884844@gmail.com>

* fix(workspace): seed dashboard workspace input

* test(workspace): cover live dashboard workspace input

* fix(workspace): backfill legacy workspace names

* test(e2e): harden workspace collaboration coverage (#6312)

Harden Workspace collaboration authority, lifecycle cleanup, and browser coverage.\n\nRetry an expired authorized Team-project pull exactly once with a fresh receipt while preserving fail-closed scope, version, cleanup, and final freshness checks. Align the fake Vela receipt lifetime with the real two-second contract and use scalable E2E timeouts.

* fix(workspace): refresh dashboard input on switch

* fix(viewer): avoid inert attribute warning

* fix(chat): preserve user turn ordering

* fix(workspace): stop retracted Team plugins immediately (#6335)

Prevent stale Team-plugin listings, materializations, and delayed positive hub reads from reactivating a resource after retraction. Preserve independent Personal plugins with the same manifest ID and keep Team members able to share their own resources.

Add deterministic regression coverage for cache invalidation, binding lifecycle, and the superseded-positive activation race.

* fix(workspace): harden Vela billing proxy boundaries (#6348)

Reject invalid wallet balances without overwriting valid cache state, validate normalized proxy paths and Workspace scope, strip hop-by-hop headers, and tear down upstream requests on disconnect.

Keep team Workspace scope fail-closed when Connection nominates the protected header.

* fix(web): tolerate unnamed workspace directory rows (#6349)

Preserve a verified explicit Workspace context when legacy directory rows omit the display-only workspaceName field. Keep exact-row name backfill when a non-empty name is present, without inferring authority from active or default Workspace state.

* fix(workspace): enforce exact project-list scope

* fix(workspace): show team design systems in pickers

* fix(web): reserve image cards while thumbnails load

* fix(web): keep home creation types ready during refresh

* test(e2e): align Windows beta smoke with installer

* perf(web): start team catalog from exact directory identity

* fix(web): refresh revoked workspace context

* fix(web): order collaboration presence refreshes

* fix(amr): prefer selected Vela OpenCode companion

* fix(web): isolate collaboration presence sessions

* fix(web): parallelize scoped project route bootstrap

* perf(daemon): cache workspace project list authority

* fix(web): preserve project-open connection budget

* test(web): preserve memory toast exports in app mock

* fix(web): preserve exact project opening scope

* fix(daemon): coalesce scoped comment pulls

* fix(daemon): retry team comment relay delivery

* fix(daemon): atomically queue comment relay writes

* fix(collab): show team comments across local conversations

* fix(daemon): preserve catalog-only projects on unshare

* fix(collab): close comment realtime catch-up gaps

* docs: add workspace team rollout runbook

* fix(collab): retry failed Team project shares

* fix(workspace): isolate personal projects in recent lists

* fix(daemon): isolate personal design systems

* fix(workspace): authorize runs before plugin snapshots

* test(daemon): align workspace retry contracts

* fix: isolate plugins and skills by workspace member

* fix(workspace): scope design systems in run prompts

* fix(workspace): gate runs before plugin resolution

* fix(daemon): namespace team design system bindings

* fix: close plugin and skill scope leaks

* test(collab): cover project-scoped comment actions

* fix(workspace): scope project resource selection

* fix(workspace): preserve personal design system reads

* fix(workspace): authorize scoped run resources first

* fix(workspace): keep project design systems scoped

* fix(types): carry design system member scope

* fix(billing): scope personal balances by workspace

* fix(workspace): preserve unbound local design systems

* fix: resolve team design systems by canonical binding

* fix(workspace): invalidate nested project catalog cache

* fix: keep team design system runs canonical

* perf: batch durable comment relay delivery

* fix: cancel stale local comment relays

* fix(web): preserve scoped project display snapshots

* fix(web): synchronize project rename projections

* perf: reuse request-scoped workspace authority

* test: enforce personal project creator ownership

* fix(web): wait for project workspace authority

* fix(daemon): invalidate team project caches before signaling

* fix(web): order targeted project metadata refreshes

* fix(workspace): preserve team catalog on outages

* fix(web): isolate catalog display across accounts

* fix(web): scope extension marketplace catalogs

* fix(web): order broad team catalog refreshes

* fix(web): scope home plugins to workspace identity

* fix(web): isolate plugin views by workspace identity

* fix(web): polish sign-in and composer focus

* fix(web): preserve scoped project state after team moves

* fix(workspace): enforce scoped design and billing identity

* test(e2e): align workspace flows with scoped bootstrap

* fix(collab): drain newer comment revisions promptly

* test(e2e): keep owner actions after workspace lock

* fix(web): converge project renames across scoped lists

* fix(web): preserve pending projects across view changes

* fix(web): restore preview runtime after activation

* test(e2e): align workspace scope assertions

* test(web): cover hidden stream recovery

* fix(web): update deep-link project titles after rename

* fix(collab): retry project metadata propagation

* fix(web): preserve legacy design system logos

* fix(web): wait for activated preview bridge

* fix(design-systems): isolate team resource storage

* fix(plugins): reject team mirror mutations

* fix(web): partition extension catalogs by workspace

* fix(skills): isolate team mirrors by workspace

* fix(plugins): keep local share projects unscoped

* fix(web): retry preview state capture during activation

* test(e2e): scope project workspace fixtures explicitly

* feat(collab): coordinate team resource invalidations

* fix(collab): emit team resource changes after sync

* fix(web): invalidate project lists after updates

* fix(web): serialize project renames by scope

* fix(collab): preserve team project comment anchors

* fix(web): refresh shared resources after remote changes

* fix(collab): materialize team projects before rename

* fix(web): keep workspace catalogs current

* fix(web): close catalog and credential races

* test(workspace): harden team workflows and packaged deep links (#6358)

* test(workspace): harden team workflows and deep links

* fix(packaged): deliver hot invite deeplinks

Generated-By: looper 0.11.7 (runner=fixer, agent=codex)

* fix(workspace): catch up team resources safely

Generated-By: looper 0.11.7 (runner=fixer, agent=codex)

* fix(workspace): invalidate resource catch-up changes

Generated-By: looper 0.11.7 (runner=fixer, agent=codex)

* fix(workspace): catch up shared resource changes

Generated-By: looper 0.11.7 (runner=fixer, agent=codex)

* fix(packaged): preserve fallback invite deeplinks

Generated-By: looper 0.11.7 (runner=fixer, agent=codex)

* fix(workspace): hide retracted team skills

Generated-By: looper 0.11.7 (runner=fixer, agent=codex)

* test(e2e): align workspace CI fixtures with route bootstrap

* fix(collab): complete design system reconnect adoption

* feat(analytics): track Workspace interactions (#6397)

* feat(analytics): track Workspace interactions

* fix(analytics): dedupe community page views

---------

Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>

* fix(daemon): preserve team comments across conversations

* fix(collab): keep presence rosters converged

* fix(comments): preserve routed comment anchors

* fix(workspace): reconcile team project renames

* fix(collab): preserve member directory last-good state

* perf(collab): start scoped Team presence before status

* fix(comments): keep relay anchors internal

* perf(collab): warm presence from daemon cache

* perf(collab): cancel obsolete cold status polls

* fix: fence presence sessions against late heartbeats

* perf(collab): share team member roster scheduling

* fix(collab): preserve roster store through strict replay

* perf(collab): retain workspace member directory stores

* fix(collab): resolve presence identities from team directory

* fix(collab): hide unresolved presence identities

* fix(collab): prefer directory presence names

* perf(web): preload workspace member directory

* test(collab): preserve no-leave lifecycle contract

* fix(web): preserve personal workspace presence after share

* fix(web): refresh covers after team materialization

* fix(web): refresh files after team materialization

* fix(web): gate shared file reads on project authority

* fix(web): preserve workspace folder action sizing

* ci(e2e): split project-workspace UI P0 shard into project-workspace + project-collab

project-workspace pins workers=1 by design (deliberate, not incidental —
see e2e/AGENTS.md's UI test stability rules on cross-file worker carry-over),
so its 7 files ran fully serially: ~26min wall time in recent merge-queue
runs, the longest pole in the ui_p0 matrix by a wide margin over its
10-13min siblings.

workspace-multi-client-collab.test.ts alone (spins up two isolated
client/daemon runtimes per case) accounted for ~10 of those minutes.
Carving it and workspace-keyboard-flows.test.ts into a new project-collab
shard — still workers=1, still one file at a time — lets the two halves
run as separate concurrent CI jobs instead of one long serial chain,
without touching the single-worker-per-shard isolation the pin exists for.

Updates the topology guards that pin the exact matrix shape so the split
doesn't drift silently: DAEMON_RUNTIME_DEFINITION_MATRIX_NAMES (scopes.ts),
the full/candidate matrix name guards (guard/scope.ts), and the fixed
expectations in scopes.test.ts / packaged-smoke-workflow.test.ts.

Validated: pnpm guard (UI P0 shadow contract + CI topology checks pass),
pnpm typecheck (all workspaces), and the two topology test files
(107/107 passing).

* fix(collab): revoke unshared team project mirrors

* fix(web): preserve unbound project authority across workspace changes

* fix(web): pair scoped raw queries with headers

* test(e2e): pin writable scope for project actions

* test(web): cover Personal authority recovery after reload

* test(web): await version preview readiness

* fix(workspace): scope brand design system resources

* fix(workspace): materialize owner team design systems

* fix(web): fence design system reads by generation

* fix(web): propagate brand read generations

* fix(web): preserve workspace project visibility

* fix(preview): retain scope for runtime relative assets

* fix(web): explain design system delete denial

* fix(daemon): bound team resource prewarm scopes

* fix(daemon): recheck prewarm lease after queue

* fix(web): stabilize transient presence roster gaps

* chore(pack): pin Vela CLI 0.0.28

* fix(workspace): move design system project with team share

* fix(workspace): gate linked unshare on live authority

* fix(workspace): ensure backing project before design share

* fix(web): scope srcdoc preview assets

* perf(web): mint preview scope only for srcdoc

* perf(web): reuse srcdoc preview scope

* chore(nix): refresh pnpm deps hashes for vela-cli 0.0.28

pnpm-lock.yaml moved @powerformer/vela-cli from 0.0.27-test.7 to 0.0.28, but nix/pnpm-deps.nix was not regenerated, so `nix flake check` failed with a fixed-output hash mismatch for both the daemon and web derivations.

Hashes taken from the Nix-reported expected values in CI run 30933223422 (nix is not available on this machine to run `pnpm nix:update-hash` locally); the merge of origin/main did not touch pnpm-lock.yaml, so those values still describe this tree.

* chore(ci): retrigger workspace-team validation

The previous ci run for this branch sat pending with zero dispatched jobs; cancelling it and pushing an empty commit re-enters the ci-6142 concurrency group from a clean slot.

* perf(web): skip preview mint for authored base

* fix(collab): preserve presence through lease gaps

---------

Co-authored-by: Ray Xi <2667192167@qq.com>
Co-authored-by: CHENGLONG WANG <honam884844@gmail.com>
Co-authored-by: lefarcen <ontf116@gmail.com>
Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Looper <looper@noreply.github.com>
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
Co-authored-by: bestthanapon <thanapon.suwannasuk@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: Amy <58060647+AmyShang-alt@users.noreply.github.com>
Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: _HDCoder <2728073932@qq.com>
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Nicholas-Xiong <2482929840@qq.com>
Co-authored-by: mehmet turac <mehmetturac@gmail.com>
Co-authored-by: open-design-release-bot[bot] <295937643+open-design-release-bot[bot]@users.noreply.github.com>
Co-authored-by: open-design-bot <bot@open-design.ai>
Co-authored-by: shangxinyu1 <shangxinyu@refly.ai>
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
2026-08-05 08:31:54 +08:00
ebcba704ef feat(prompts): introduce on-demand discovery in SP v2.0 (#6223)
* feat(prompts): introduce on-demand discovery in SP v2.0

* fix(prompts): prevent nested deck label overlap

* fix(plugins): stabilize od-default task routing

* fix: address prompt and deck audit regressions

* fix(prompts): restore plan mode precedence

* fix(daemon): accept all form answer headers

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-31 02:35:18 +00:00
bestthanaponandGitHub f9d42f4770 Add Clone Audit plugin (#5687)
* Add Clone Audit plugin

* Update open-design.json

* fix: make clone audit skill reusable

* fix: sanitize clone audit provenance path
2026-07-29 04:08:31 +00:00
VERSATILandGitHub 1912c3ba97 Add Atelier Zero — Image Generation Prompt Pack plugin (#5873) 2026-07-22 06:18:52 +00:00
5435274ab4 Add Humanize PPT plugin (#5655)
* feat(plugins): add Humanize PPT community plugin

Ports LearnPrompt/humanize-ppt (MIT) into plugins/community/ following
the community/hallmark precedent, and registers it in the community
marketplace index (0.2.0 -> 0.2.1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(plugins): harden humanize-ppt per review — out-dir guard, PPTX fail-closed, packaging (v1.1.1)

- brief mode no longer wipes a non-empty --out unless it carries Humanize
  run markers or --force is passed; wipe semantics documented in SKILL.md
  and docs/local-demo.md
- read_source fails closed on .ppt/.pptx with guidance (extract text
  first; rendered decks go to --qa-from); SKILL.md/SPEC.md wording aligned
- vendor examples/01-ai-tool-update/source.md, inline-fixture fallback in
  smoke_check.py, drop repo-only record_demo_gif.py, point dangling doc
  links at the upstream repo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(plugins): humanize-ppt review follow-ups — outline-gate markers, shim exit codes (v1.1.2)

- HUMANIZE_OUT_MARKERS now includes outline-preview.md and
  preview-confirmed.json, so the documented --preview-outline ->
  --confirm-outline -> brief flow on one --out is no longer refused by
  the out-dir guard; refusal text, --out help, SKILL.md/SPEC.md/
  docs/local-demo.md list all four markers
- humanize_ppt_v3/v4/v5 shims call sys.exit(main()) like the stable
  entrypoint, so validation failures propagate exit 2 instead of 0
- smoke_check.py gains an outline-gate check (preview -> confirm ->
  brief on one --out, plus the unrelated-out refusal negative path) and
  an exit-code matrix across all five public entrypoints
- plugin 1.1.2, community registry 0.2.3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:52:47 +00:00
netteeandGitHub 6b90486c97 docs: reconcile stale documentation with current code (#5829)
* fix(docs): align question guidance with form flow

* docs: reconcile stale documentation with current code

* fix(docs): remove retired template metadata from guides

Generated-By: looper 0.10.6 (runner=fixer, agent=codex)

* fix(e2e): stabilize visual workspace helper

Generated-By: looper 0.10.6 (runner=fixer, agent=codex)

* fix(docs): align localized template guide metadata

Generated-By: looper 0.10.6 (runner=fixer, agent=codex)
2026-07-18 03:41:08 +00:00
open-design-crew[bot]GitHubNJUHuaMasonopen-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
4b660237c0 feat(prompts): land the slim system-prompt line as the default charter (#5603)
* test(daemon): freeze scenario x section gating matrix for composeSystemPrompt

composeSystemPrompt splices ~30 conditional sections whose gates form an
implicit matrix (session mode x design system x skill surface x memory x
execution profile). Before any prompt-slimming refactor, this golden
snapshot freezes which sections each representative scenario receives,
so a re-gating regression surfaces as a snapshot diff instead of a
silent production behavior change.

Detection is line-anchored heading markers: discovery quotes headings
like "## Active plugin" mid-sentence, so bare substring matching would
false-positive on sections that were never spliced in. A companion
assertion fails when a marker goes stale (no scenario produces it), and
position invariants pin injection-resistance first / role-marker guard
last across every scenario.

* perf(prompts): stop shipping rare-path media/deck payloads on every run

Langfuse telemetry (24h, 2k-trace sample) puts the composed charter at
p50 96KB (~24K tokens) with a 21% stable-cache hit rate — most turns
resend it whole. Two always-on blocks serve paths most runs never take:

- MEDIA_DISPATCH_HINT (2.8KB into every non-media project) embedded a
  40-line worked bash polling loop. The loop moves to 'od media help'
  (the CLI already prints the exact next wait command on handoff), and
  the hint keeps only the dispatch rules agents historically fumbled:
  no provider REST calls, no API-key asks, POSIX not PowerShell,
  python3 not jq, fal-ai/* passthrough. 2,771 -> 1,547 chars.

- The freeform maybe-deck branch injected the full 20.7KB deck
  framework into every kind=other / no-metadata run 'just in case'.
  New detectDeckIntentSignal() scans the same request text the agent
  will see (transcript included, so a deck mention anywhere in the
  conversation stays sticky) for generous EN/CJK deck vocabulary and
  skips the framework when nothing matches: 78.5K -> 57.7K chars on
  non-deck freeform runs. False positives fall back to today's
  behavior; deck-kind projects stay unconditional. An absent signal
  (contracts/BYOK composer, tests) preserves legacy always-inject.

Also relieves the POSIX argv budget: DS-active runs sat within 4KB of
the 100KB hard cap that fails argv-bound adapters (aider/deepseek).

Baseline check: tests/media/policy-routes.test.ts (7) and
tests/connection-test.test.ts (1, local Kimi CLI version) fail
identically on the unmodified tree — no new failures from this change.

* test(daemon): refresh golden matrix snapshot for upstream memory prompt hardening

Rebase onto main picked up #4913, which reworded the memory
rule-proposal block (+230 chars in memory-bearing scenarios). Section
gating is unchanged — size-only refresh.

* feat(prompts): rewritten slim core charter behind OD_PROMPT_CORE=slim

The always-on doctrine layer (DISCOVERY_AND_PHILOSOPHY ~28K chars +
designer charter ~14K + three tail overrides that re-assert rules the
layered stack states in softer or contradictory forms) is rewritten as
ONE ~15.5K-char document: core-slim.ts. Same composed inputs drop from
65.7K to 36.0K chars (-45%); freeform non-deck runs from 57.7K to 28.9K.

Design rules of the rewrite:
- Every rule stated exactly once, under an explicit precedence ladder
  (user request > active skill > design system > memory > charter).
- Contradictions resolved instead of stacked: one verification budget
  (static pass + P0 checklist + craft scan + at most ONE render; the
  'two passes is normal' re-score loop is gone), one turn-1 form
  decision path (four skip conditions listed in one place, including
  the memory task-brief replacement), Tweaks reconciled (iteration
  surface on request, never demo controls in product files).
- Protocol markers are frozen API and survive verbatim: question-form
  shapes/ids, pick_direction/brand_spec/reference_match branch values,
  task-type routing options, data-od-id, EDITMODE markers, pinned
  React/Babel/Motion tags, artifact handoff shapes.
- Emphasis quota: no IMPORTANT/CRITICAL inflation; imperatives carry
  the weight.

Rollout: ComposeInput.promptCoreVariant ('classic' default | 'slim'),
daemon flips via OD_PROMPT_CORE=slim — zero behavior change until set,
A/B-able per daemon. Ask mode keeps the clarifying-questions tail (it
composes no core charter). Guards in tests/prompts/core-slim.test.ts:
a 16KB byte budget that fails any silent regrowth, frozen-marker
assertions, handoff profile switch, and classic-parity checks; matrix
snapshot gains slim scenarios.

Not yet covered (follow-ups): the packages/contracts BYOK composer
still ships only the classic stack; instruction-following A/B against
classic is the sign-off gate before changing the default.

* feat(prompts): shrink the slim core to an 8KB budget by evicting what a system prompt shouldn't carry

The 15.5K v1 slim charter still carried content that has no claim to an
always-on seat. v2 evicts it by category:

- Already owned elsewhere: the od-default task-type router form ships in
  plugins/_official/scenarios/od-default/SKILL.md and reaches the prompt
  as '## Active skill'; the charter now just defers to skill-owned
  turn-1 forms. An ownership test pins the form to the skill file.
- Conditional-path content: per-platform delivery contracts (breakpoint
  matrix, per-target files, native chrome) move to
  PLATFORM_CONTRACTS_BLOCK, injected only when the project declares a
  platform or multiple targets. Deck rules were already duplicated by
  the deck-gated directive - dropped from the charter entirely.
- Model-competence content: PDF extraction how-tos, attached-image
  explanations, JSON syntax reminders - deleted.
- Recital: the default discovery form example keeps only the brand
  question verbatim (its pick_direction/brand_spec/reference_match
  values are the one protocol-matched part); the other six questions
  become a one-line recipe. Option labels were always model-localized
  per language, so exact English menus were never stable anyway.
- React pins keep versions (the actual bug fix: 18.3.1 pairing +
  framer-motion React build) but drop the SRI hashes and script-tag
  boilerplate - reviewers should weigh the integrity-attribute loss.

Charter now renders at 8,187 bytes (budget test tightened 16KB -> 8KB).
Composed prompt for the common design path: 65.7K classic -> 29.6K slim
(-55%); freeform non-deck: 57.7K -> 21.5K (-63%).

* perf(prompts): evict the remaining always-on payloads from the slim stack

Follow-up to the 8KB charter: the composed slim prompt still carried
~13K of non-charter payload. Four evictions, all slim-only (classic
stays byte-stable for the A/B):

- Project metadata becomes a fact sheet ('facts' style). The classic
  block had grown seven doctrine rules plus a responsive contract and
  cross-platform rule - prose owned by the slim charter's Craft section
  and PLATFORM_CONTRACTS_BLOCK. facts keeps key-value fields, media
  workflow data, and two compact delivery lines (screen files, product
  depth). kind=other: 3.6K -> ~1.1K.
- Memory two-loop scaffolding compressed ~5.5K -> ~2.9K: identical
  headings and od-card shapes (web parses the card types; the daemon
  checks the verify-scorecard), repeated rationale prose cut.
- zh-CN locale override drops the worked quick-brief sample copy under
  slim - it mirrored the classic form verbatim, which slim recipes
  instead; the form contract already mandates localization. 2K -> 0.85K.
- MEDIA_DISPATCH_HINT gates on detectMediaIntentSignal (same
  transcript-sticky pattern as the deck signal): most conversations
  never mention media, and the hint returns for the rest of the session
  the moment one does.
- Direction library becomes a pull layer: slim composes a ~1.3K
  id+label index and the agent fetches the chosen direction's full
  palette/posture spec via the new 'od tools directions --id <id>'
  CLI (also --label / --json / bare list). One tool call on the
  direction-pick path, which runs at most once per project, replaces
  6.7K of always-on palette data. Classic keeps the inline library.

Composed slim sizes: freeform non-deck 21.5K -> 12.6K; design full
stack 29.6K -> 22.2K (fixture DS; media hint drops another 1.4K on
non-media conversations). Baseline classic: 57.7K / 65.7K.

* refactor(prompts): restructure the slim prompt as static-first layers for cache and hierarchy

Two structural defects in the slim composition, both flagged against
prompt best-practice guidance:

1. Heading hierarchy was inverted: the document opened with an H2
   ('## Security: prompt injection resistance') and the H1 charter
   appeared six sections later. The security section now lives INSIDE
   the charter as an H2 right after Precedence (single source moved to
   core-slim.ts; the classic stack imports it for its unchanged
   standalone-first position), so the composed document reads
   identity-first with a coherent H1 > H2 tree.

2. Ordering was cache-hostile: conditional blocks (mode override,
   locale, direction index, platform contracts) preceded the static
   charter — the exact 'conditional prefix sections' anti-pattern from
   the prompt-caching guidance, where every flag combination becomes a
   distinct prefix and nothing is shared. Slim now composes in
   variability tiers:

     static charter (identical across all runs — shared cacheable
     prefix) -> conversation-stable (mode, locale) -> project-stable
     (direction index, frames, platform, memory, instructions, DS,
     skill, metadata, MCP) -> turn-variable (maybe-deck and media-hint
     signal blocks, deferred to after MCP so a mid-conversation signal
     flip only invalidates the suffix) -> recency-pinned role guard.

   The charter's Precedence section now states that a session-mode
   directive appearing after it takes precedence, replacing the
   positional 'read first' authority the old ordering relied on.

Classic composition is byte-untouched (A/B baseline); ask mode keeps
the classic head. Charter budget 8KB -> 9.25KB (absorbed the ~1KB
security section; composed totals unchanged: freeform 12.7K,
full-stack 22.4K). New layered-ordering test pins the tier sequence
with line-anchored markers; the matrix position invariant now asserts
the per-variant head.

* fix(prompts): resolve five structure/expression defects from the charter review

Findings from the senior-review pass, each with a concrete failure mode:

1. Turn-ordinal section names misfire mid-session. 'Turn 1/2/3+' made
   the model treat 'now is not turn 1' as license to skip the fresh-
   brief flow when a NEW artifact is requested at turn 8 — and,
   conversely, an event-based fix alone would have re-asked questions
   the session already answered. Sections are now event-named ('On a
   fresh brief' / 'When the brand answer arrives' / 'Once direction is
   locked') AND a session-inheritance rule makes the mid-session case
   explicit: a fresh brief inherits the conversation's locked
   direction/brand/audience, asks only genuinely new unknowns, and
   builds directly when there are none — matching current production
   behavior with zero extra rounds or cache cost.

2. The handoff rule — the single highest-cost-if-violated output
   protocol (never emit <artifact> in filesystem runs) — was the only
   rule block without a heading. Promoted to '## Handoff'.

3. The form example read backwards: it showed the LAST question
   (brand) and described the earlier six in reverse ('preceded
   in-form by'), the only place in the prompt requiring the model to
   assemble structure in reverse. Now forward-order: output question
   shown in full, brand question shown in full (its values are the
   protocol-matched part), the middle questions listed as
   insert-between recipes.

4. Dangling reference: Craft said 'single family only for
   tech/utility' — a direction name defined only in the Direction
   index, which is absent whenever a design system is active.
   Reworded direction-independent.

5. direction-cards was dead vocabulary: listed as a legal control but
   every mention forbade it. Its one legitimate path is now named —
   only when the user explicitly asks to see direction options.

Charter budget 9.25KB -> 10KB: fix #3 cost ~0.7KB (restoring the full
output-question JSON), accepted deliberately — it also restores
few-shot density for weaker fleet models, and the review's own
conclusion was that the last byte-squeezing round had negative ROI.

* fix(prompts): identity-first head for slim ask mode

The last remaining hierarchy inversion: slim ask still opened with the
H2 security block before the H1 ask override. Ask now matches design
mode's structure — the ask override (the turn's whole charter) opens
the document and the security section reads as its first subsection.
Both blocks are static, so the swap is cache-neutral. Classic remains
byte-untouched.

* fix(prompts): repair five slim-vs-classic regressions from the second audit

Second senior review against the shipping classic stack surfaced two real
bugs and four wrongly-compressed classic behaviors in the slim variant:

- text_artifact/BYOK runs got the direction INDEX plus an instruction to
  run the `od tools directions` CLI they do not have; classic inlined the
  palettes. Slim now profile-switches: filesystem keeps the 1.3K pull
  index, text_artifact gets the full inline library back (+1.35K tokens
  on that path only; still ~68% below classic, and now under deepseek's
  30KB Windows argv cap, which classic never was).
- API_MODE_OVERRIDE's literal scope ('every LATER instruction … is
  overridden') inverted when slim moved the charter ahead of it: the
  charter's TodoWrite/render-check instructions escaped the override.
  Plain-stream slim runs now compose the override first again. Pure
  reorder, zero bytes.
- PLATFORM_CONTRACTS_BLOCK gated only on project metadata, missing
  freeform briefs that name a platform in text ('做个 iOS app 原型').
  New detectPlatformIntentSignal (same generous EN/CJK shape as the
  deck/media signals) ORs into the gate; server wires it.
- media-contract's 'explicit layer overrides' paragraph cross-referenced
  the classic-only layer names; reworded layer-agnostic (classic +19B).
- Restored four classic rules the compression pass misfiled as
  model-competence filler: never hot-link user-attached images by URL
  (product constraint), skill/DS per-domain precedence clause, expressive
  form-control guidance, and the modern-CSS encouragement line. Charter
  budget 10,240 -> 10,752 with rationale in the test comment.

Classic bytes untouched except the shared media-contract rewording.
Golden matrix: gating unchanged; sizes only (classic +19, slim +537).

Pre-existing failures unrelated to this change and failing on the
unmodified baseline: tests/media/policy-routes.test.ts (7), Kimi
connection test (local CLI version mismatch).

* fix(prompts): defer signal-triggered platform contracts to the turn-variable suffix

Two follow-ups from the third structure audit:

- PLATFORM_CONTRACTS_BLOCK sat in the project-stable zone while its gate
  now includes the turn-variable conversation-text signal. A mid-session
  'make it an iOS app' would insert the block early and break the cached
  prefix for every section after it (DS, skill, memory, metadata, MCP).
  The gate is now split by trigger stability: metadata triggers keep the
  early slot (fixed at project creation), signal-only triggers defer to
  slimTurnVariableParts like the deck/media signal blocks. The
  slimTurnVariableParts declaration hoists above the design section so
  the gate can reach it; no byte or ordering change for any existing
  gating combination.

- Slim ask mode on a plain stream composed CHAT -> injection -> API
  override, inverting classic's authority order (API before CHAT, so
  'overrides every rule below' covers the chat charter). The ask head
  now leads with the API override on plain streams; the later push is
  gated on !isSlimCore. Zero-byte reorder; the two overrides do not
  conflict in content, this restores parity with the shipping order.

* fix(prompts): restore form-tailoring mandate and first-message form scope

The slim-vs-classic blind A/B eval (2026-07-04, 17 pairs, double-order
opus judging) surfaced two form regressions with converging judge
rationales:

- Form quality lost 12:1:4 to classic. Mechanism: classic's discovery
  layer carries extensive per-brief form-tailoring guidance; slim
  compressed it to the single word 'tailor'. Models shipped the default
  recipe verbatim — app-platform checkboxes on landing-page briefs, the
  fundraising ask/traction buried in one textarea placeholder. Restored
  as an explicit mandate with concrete per-brief examples, a
  plausible-targets-only rule for the platform options, and a
  translate-the-labels note on the tone recipe.
- DS + detailed-brief projects skipped the turn-1 form 3/6 vs classic
  1/6: the mid-session inheritance clause generalized to first
  messages. Added the explicit scope limit: inheritance never applies
  to a project's first message.

Charter budget 10,752 -> 11,264 with the eval citation in the test
comment. Golden matrix: sizes only.

* fix(prompts): template tokens in the seed must be replaced before handoff

The 40-task x 5-turn A/B eval (2026-07-05) found slim's remaining T2
quality gap is driven by catastrophic failures, not taste: 3 of 39
pairs shipped the live-artifact skill seed with unrendered {{mustache}}
tokens (classic did this once). Mechanism: slim compressed classic's
seed-handling guidance to 'copy the seed, paste layouts', losing the
explicit fill-with-real-content step.

- Read-before-write bullet now ends with: replace EVERY template token
  with real content; a {{placeholder}} or empty section reaching the
  user is a failed delivery. (Also covers the eval's 4 thin-content
  failures.)
- Self-check static pass adds 'leftover template tokens' to its scan.

Charter 11,155B, within the existing 11,264 budget — no bump needed.

* fix(prompts): first build ships complete, no stub sections

Clean-scope T2 analysis of the 40-task eval (excluding the 10 tasks
confounded by the live-artifact skill protocol) still shows a first
build completeness gap: main 7.34 vs slim 6.10, driven by thin-content
deliveries (empty canvases, placeholder-looking blocks) on 4 of 29
pairs. The charter's 'show something visible early' line, uncushioned,
reads as license to hand off the early skeleton.

- Qualify it: '…but the turn ships the COMPLETE artifact: no stub
  sections.'
- Self-check static pass adds 'empty or stub sections' to its scan.

11,215B, within the 11,264 budget.

* refactor(prompts): split the overloaded turn-1 form section by concern

The slim charter's '## On a fresh brief' H2 bundled six distinct
concerns under one heading — trigger, default action, mid-session
inheritance, skip conditions, form-shape tailoring, and the form JSON
contract — so a maintainer could not scan the outline to find any one
rule, and the cross-cutting form contract (valid on ANY turn) was buried
under a heading scoped to fresh briefs.

Behavior-equivalent restructure (every directive's wording preserved;
only headings added and the form contract's 'any turn' note moved from a
parenthetical to the section lead):

- ## Turn 1 — the discovery form        (trigger + default action)
  - ### When to skip or inherit the form (inheritance + skip conditions)
- ## Writing a `<question-form>`         (cross-cutting: shape + tailoring + example + ordering)
  - ### Form contract                     (JSON validity + type enum + localization)

The charter outline now reads as a per-concern document. Frozen protocol
markers (form JSON, brand values, type enum, allowCustom) unchanged.
Charter 11,242 -> 11,322B; budget raised to 12,288 for readability
headroom at the maintainer's direction (budget explicitly deprioritized
for this change). Golden snapshot regenerated; 113 prompt tests green.

* refactor(prompts): readability second pass + multi-turn DS/constraint adherence

Two changes, from a critical re-read plus production feedback.

A) Readability / logic second pass (behavior-equivalent, wording preserved):
- Heading style: the two remaining imperative-tail H2s became event
  labels with the directive moved to the body lead ('## When the brand
  answer arrives', '## Once direction is locked'), so the outline reads in
  one consistent grammar. Dropping '…hand off' also fixes a heading that
  promised handoff while the rules live in the next section.
- The self-check bullet's inline (1)(2)(3)(4) run-on became a four-item
  sub-list — the least-scannable line is now the most scannable.
- The 70-word 'Read before you write' sentence split into shorter units;
  'show early / ship complete' separated into its own bullet so the two
  ideas stop reading as contradictory.
- Precedence: skill and DS collapsed into one rank split by domain (was
  numbered 2 vs 3, contradicting the 'each highest in its own domain'
  clause) — removes the only real logical seam in the ladder.

B) Multi-turn edit adherence (new '## On an edit or tweak' section):
  Production feedback reports the design system and explicit user
  constraints frequently drift during multi-turn edits. The charter was
  turn-1/first-build heavy and had no standing edit-mode invariant. Added,
  in the lifecycle between build and handoff:
  - an edit touches ONLY what the user named; untouched sections/values
    stay as they were; re-read the file and edit in place.
  - the design system binds on EVERY turn, not just first build — no
    drift, no raw hex, no palette re-pick because this turn was about
    something else.
  - every hard constraint stated this session persists across later turns
    until the user relaxes it; a new request adds to constraints, never
    silently resets them.

Charter 11,322 -> 12,029B, within the 12,288 budget (no raise). New
frozen assertion pins the adherence invariants against future
compression. 114 prompt tests + guard + typecheck green; golden snapshot
regenerated.

* refactor(prompts): two-tier charter — lifecycle spine, grouped pillars, precedence precision

From a senior word-by-word re-read. The charter was 13 flat H2s spanning
three conceptual tiers (foundations / lifecycle / reference) with no
visible spine, and the precedence wording could invert authority.

Structure (behavior-equivalent, rule wording preserved):
- 13 flat H2 -> 5 H2: two foundations (Precedence, Security) + three
  pillars, each grouping its H3s —
    ## Discovery — ask before you build   (Turn 1, skip/inherit, writing, contract)
    ## Delivery — brand → build → iterate  (brand answer, build, edit, handoff)
    ## Craft & contracts                    (craft, technical, conduct)
  The delivery lifecycle is now one connected, ordered spine — 'Editing
  an existing artifact' reads as a named phase of it, which should help
  the multi-turn drift the edit-adherence rules target.

Precision fixes:
- Precedence: 'On conflict, higher wins: 1…2…3…4' was ambiguous (which
  number is higher?) — reworded to an explicit list, 'the one higher on
  this list wins — the user's request is the highest authority, this
  charter the lowest', and the trailing pronoun tightened to 'overrides
  it wherever the two conflict'.
- 'no tool calls or file reads first' -> 'before it'; 'Resolve the
  source' -> 'Resolve the brand source'; 'restyle the whole thing' ->
  'restyle beyond the request'.
- Trimmed one of three localization restatements (kept the authoritative
  one in Form contract, now naming labels/options/placeholders) and the
  'however complete the brief reads' duplicate.

Handoff/Craft/Technical/Conduct headings demote to H3 under their
pillars (local constants, not shared with classic). Charter 12,029 ->
12,294B; budget 12,288 -> 13,312. Ordering/adherence test anchors and
golden snapshot updated. 114 tests + guard + typecheck green.

* fix(prompts): make edit-adherence forceful — do exactly what was asked, verify it landed

Production feedback and maintainer review: the edit section addressed
only scope (don't over-change) and persistence (DS/constraints), but not
the core instruction-following failure — 'the user asked for A and the
result isn't A'. The whole section was defensive; the positive imperative
that the requested change must actually happen, completely, was missing.

Strengthened '### Editing an existing artifact':
- Leads with the imperative: the delivered file must actually be A — do
  exactly what was asked, in full, in every place it applies, with
  complete-vs-partial examples ('dark green' = every element, not one).
- Forbids reinterpreting, 'improving on', partially applying, or
  substituting own taste for the user's literal words — ties to
  Precedence #1 (user's request is highest authority).
- New 'Verify before you hand off' bullet: open the file, confirm the
  change is actually present and complete and no in-force constraint
  broke; never report a change you did not make (the claim-vs-reality
  failure).
- Kept the scope / DS-binding / constraint-persistence bullets.

Charter 12,294 -> 13,122B, within the 13,312 budget. Frozen assertion
updated to pin the positive imperative and the verify bullet.

* fix(prompts): a later explicit request overrides a conflicting earlier constraint

Edge case raised in review: turn 2 'fix the color to blue, don't change
it', turn 4 'change everything to yellow'. Correct behavior is turn 4
wins (the user is changing their own mind; yellow becomes the new
standing constraint), which Precedence #1 + 'persist until the user
relaxes it' already imply — but the old bullet also said 'a new request
ADDS to those constraints', which a literal model could read as
additive-only and stall on the turn-2 'don't touch it'.

Rewrote the constraint-persistence bullet to make the override explicit:
constraints carry forward, only the user can lift or change one, and a
later explicit request overrides a conflicting earlier constraint — with
the blue->yellow turns as the in-prompt example. The anti-silent-drop
guard (model must never drop a standing constraint on its own) is kept.

Charter 13,122 -> 13,412B; budget 13,312 -> 14,336.

* fix(prompts): make the plan step agent-agnostic — TodoWrite is Claude-only

Open Design drives many local code agents; the real session mix is ~65%
non-Claude (codex, opencode, gemini, ACP family, …). The slim charter's
plan step hardcoded 'first tool call is TodoWrite' + in_progress/completed
— a Claude Code tool those agents don't have. For ~2/3 of traffic that
instruction is dead: the agent either skips planning (losing the exact
lever that helps multi-turn adherence) or hallucinates a tool it lacks.

Everything else in the slim charter was already agent-agnostic (render
checks and direction pulls go through the shell OD CLI wrapper; protocol
markup is assistant text), so TodoWrite was the one hardcoding.

- Charter plan step → generic: 'lay out a short, updatable plan; if your
  runtime has a structured plan/todo/task tool, use it, else a numbered
  list; never call a tool you don't have.'
- Composer injects a concrete note ('Your plan tool is TodoWrite; the
  host renders it as a live Todos card; mark in_progress/completed') only
  for Claude-family runs (streamFormat claude-stream-json: claude,
  codebuddy, amp) — the only agents with TodoWrite and the host's live
  Todos card. Agent-specifics live in the composer, portable doctrine in
  the charter.
- API_MODE_OVERRIDE reframed from 'TodoWrite/Read/… are unavailable' to
  'any tool call — TodoWrite/…/or whatever your runtime exposes — will
  not execute'. Keeps the specific names (issue #313 anti-pseudo-tool,
  test-pinned) while covering non-Claude runtimes.

Two frozen tests pin: charter has no TodoWrite + generic wording;
Claude-family gets the note, json-event-stream doesn't. 116 tests +
guard + typecheck green. Charter 13412 to 13543B (budget 14336).

* feat(prompts,live-artifacts): render one-level repeats, tighten slim charter craft guards

- live-artifacts/render: implement the documented `data-od-repeat="item in data.path"`
  one-level repeat directive (specified in the schema but never rendered), with
  loop-scoped `{{item.*}}` bindings, single-pass expansion so data values are never
  re-interpolated, and explicit rejection of nested repeats / non-array sources.
- live-artifact SKILL.md: state the html_template_v1 binding contract in the workflow
  (flat data.* scalars + data-od-repeat lists; no hand-loops, no bare loop vars) so
  templates stay within the renderer's capability.
- core-slim charter: "replace every template token" now yields to a skill's
  template-plus-data protocol; add an objective Layout-integrity craft rule (no
  overlap, no clipping, oversized display type must fit its column, charts use fills
  not outlines) and name those failure modes in the self-check.

* feat(prompts): add agent-agnostic production-value craft rule to slim charter

Visual-first pages regressed on slim: hero images were hand-drawn CSS boxes
instead of generated art (P1), and controls read flat/greyscale (P5). The slim
charter had dropped all of main's imagery + visual-system guidance.

Add one "Production value — feel shipped, not greyscale" craft rule: generate a
real image via the Open Design media tool (referenced through the injected
`"$OD_NODE_BIN" "$OD_BIN"` wrapper, so it is runtime-agnostic — not tied to any
one code agent) when a hero/product shot would carry the page; ship a real
product palette, colored interaction states, and elevated primary controls.

Bump SLIM_CORE_BYTE_BUDGET 14_336 -> 15_360 to seat this load-bearing guidance.

* feat(prompts): generalize the production-value imagery rule beyond hero shots

40-task eval + user review (C3 tea gift-box, P1) showed slim under-generates real
imagery on visual-first work, defaulting to hand-drawn schematic graphics. Reframe
the rule from a narrow "hero/product shot" trigger to a general default: whenever a
real picture would lift the artifact (product, place, food, person, hero, texture),
generate it via the media tool; drop to a diagram/UI mock only when that genuinely
serves the content better. Still agent-agnostic (media tool via the injected
"$OD_NODE_BIN" "$OD_BIN" wrapper).

* fix(prompts): let the production-value rule use the run's own image generation

Hard-pointing imagery at `od media generate` only works when a daemon spawned the
agent (it injects OD_PROJECT_ID); a standalone agent (eval harness, external
runtime) can't reach it, so slim silently fell back to hand-drawn placeholders
while codex-run peers used their native imagegen. Broaden the rule: generate through
whatever image generation the run offers — the Open Design media tool when wired up,
otherwise the runtime's own native image generation. Verified: C3 slim now emits a
real generated product photo instead of a schematic box.

* test(daemon): drop connected-external-mcp from the golden matrix

Upstream moved the connected-external-MCP directive out of
composeSystemPrompt into the per-turn slice (live OAuth token state must
stay out of the cached stable prefix), so it is no longer a compose
input or a gateable section.

* chore(prompts): default to the slim charter on the validation branch

VALIDATION-ONLY commit for feat/system-prompt: packaged beta builds must
exercise the rewritten slim charter, and the packaged sidecar env
allowlist does not forward OD_PROMPT_CORE, so the default flips to slim
here. OD_PROMPT_CORE=classic restores the classic stack. Do not carry
this default into main.

* perf(prompts): batch slim tool work, with quality guards restored (#5555)

* perf(prompts): batch slim tool work

* fix(prompts): restore seed-copy rule and unconditional own-browser ban

The tool-economy pass (02923153a) dropped two quality instructions as
collateral while compressing the read/preview bullets:

- 'Copy the seed and paste its layouts — don't write CSS from scratch'
  guards the visual floor when a skill ships a seed template.
- The hard 'never your own browser' ban was softened to 'no browser
  probes first', which let the candidate arm reach for Playwright after
  an export failure (2026-07-13 slim-tool-economy eval, v1_001 turn 3).

Restore both inside the batched tool-economy wording, raise the charter
byte budget 15_360 -> 15_616 following the existing restore-precedent,
and freeze both lines as regression markers.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* feat(prompts): prefill question forms with a recommended default per question (#5556)

Every <question-form> the agent emits now ships a brief-inferred
recommended `default` on each question — an option value for
radio/select, an array of values for checkbox, concrete text for
free-text fields — so the user can review and submit the form unchanged
instead of answering from scratch. Applied across the slim charter form
contract, the full discovery rules, and the od-default task-type router
(whose options stay verbatim; prefill is the one permitted tailoring).
Both prompt mirrors (daemon + contracts) updated in sync, with the
example forms anchoring the pattern via "default": "pick_direction".

The web renderer already honoured defaults, with one gap: the `default`
alias only accepted scalars, so a checkbox array default was silently
dropped (parseDefaultValue). Fixed alongside, with parser coverage for
the radio/checkbox/text shapes the prompt now instructs.

Charter byte budget raised 15_616 -> 15_872 for the new bullet +
example anchors; prefill markers frozen in core-slim and
discovery-form tests.

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* fix(prompts): make form prefill survive streaming key order; add web search/fetch imagery fallback (#5558)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* chore(prompts): land #5573 form refinements on the system-prompt validation line (#5575)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

* feat(forms): Other-chip escape hatch, 5-question cap, form-language copy, and streamed-default truncation fix (#5573)

* feat(forms): host-owned Other chip, 5-question hard cap, and form-language host strings

Three form-UX upgrades that shipped together on the validation branch:

1. Host-owned "Other" escape hatch. Finite-choice questions no longer show
   an always-on "Custom answer" input; the host injects a localized,
   dash-bordered "Other" chip (an "Other…" entry for selects) that expands
   into the type-in field via the shared accordion motion. Opening it on a
   single-choice question deselects the fixed options; picking a fixed
   option collapses a still-empty field; checkbox custom entries coexist.
   Prompts (slim charter + discovery mirrors) now ban model-authored
   catch-all "Other …" options — they would duplicate the host's — and the
   od-default router's taskType question pins allowCustom: false in all
   three copies since its own "Other" option IS the route.

2. Hard cap: 5 questions per form. The old "keep ≤7" guidance let the
   model pad forms to the example's length. The rule is now an explicit
   count-then-cut step ("count your draft; delete the least build-critical
   until 5 remain"), and the discovery example itself was trimmed from 7
   to 5 questions so the anchor matches the cap.

3. Copy + language. Form copy anchors lead with the benefit ("Prefilled
   for you — send as is, or tweak anything first"); the localization rule
   demands native phrasing with a concrete wrong-vs-right anchor (快速确认,
   not the literal 快速简报); and forms now declare a top-level "lang"
   BCP-47 tag that the web card uses to render its own strings (Other
   chip, custom-answer copy, submit CTA) in the form's language instead of
   the UI locale — no more English "Other" chips inside a Chinese form.
   New i18n key qf.otherOption ships in all 19 locales.

Charter byte budget 15_872 → 16_896 across the two documented bumps, with
frozen-marker tests pinning the escape-hatch delegation, the hard cap, the
native-localization rule, and the lang anchor.

* fix(web): never surface a mid-stream question-form default

The streaming shaper reveals a question the moment its label parses, and
runs every question object — including the trailing, still-open one —
through mapRawQuestion. That let a MID-STREAM `default` reach the card:
the partial-JSON repair terminates it early, so "单位教育" surfaces as
"单位教" and ["历史背景与经过", "抗战精神与当代意义"] as ["历史背景与经过",
"抗战精"]. The card adopts a streamed default only while the answer is
still empty, so a truncated adoption freezes garbage that the completed
value can never overwrite — checkbox defaults (long arrays, near-certain
to be cut across delta chunks) effectively never prefilled, while short
radio scalars usually survived inside one chunk. Observed in production
run beaf2da0 (抗日主题汇报PPT): both maxSelections checkboxes rendered
with nothing selected despite the model emitting complete defaults.

Fix at the source: the trailing in-flight question exposes NO
defaultValue until its braces close. A closed object's braces are
balanced, so its default is complete; the card's empty-and-untouched
adoption rule then fills it exactly once, correctly.

Red spec reproduces the production body cut mid-default (went red on the
pre-fix tree) and pins that a closed question's default surfaces while
the next in-flight question's stays hidden.

* fix(prompts): align task-type form contract across all three copies; fit the slot menu inside the 5-question cap

Review follow-ups from PR #5573 (nettee):

- The od-default skill's task-type form now carries the same top-level
  "lang" tag as the daemon/contracts prompt copies, so host-rendered
  controls on that path key off the form language instead of falling back
  to the app locale. A new parity check reads all three copies and locks
  both contract keys ("lang" + taskType allowCustom: false) so the skill
  copy cannot drift again.

- The slim charter's default-shape recipe contradicted the new hard cap:
  "Between output and brand, in this order: platform, audience, tone.
  After brand: scale, then constraints" prescribed 7 questions in the
  same breath as "Hard cap: 5". The recipe is now 2 fixed slots plus a
  pick-AT-MOST-3 menu (same option vocabulary, same order), and the
  frozen-marker test asserts the menu wording is present while the old
  prescriptive sequence is gone — the charter can no longer carry both.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>

* chore(prompts): land #5586 photo-overlay discipline on the system-prompt validation line (#5600)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

* feat(forms): Other-chip escape hatch, 5-question cap, form-language copy, and streamed-default truncation fix (#5573)

* feat(forms): host-owned Other chip, 5-question hard cap, and form-language host strings

Three form-UX upgrades that shipped together on the validation branch:

1. Host-owned "Other" escape hatch. Finite-choice questions no longer show
   an always-on "Custom answer" input; the host injects a localized,
   dash-bordered "Other" chip (an "Other…" entry for selects) that expands
   into the type-in field via the shared accordion motion. Opening it on a
   single-choice question deselects the fixed options; picking a fixed
   option collapses a still-empty field; checkbox custom entries coexist.
   Prompts (slim charter + discovery mirrors) now ban model-authored
   catch-all "Other …" options — they would duplicate the host's — and the
   od-default router's taskType question pins allowCustom: false in all
   three copies since its own "Other" option IS the route.

2. Hard cap: 5 questions per form. The old "keep ≤7" guidance let the
   model pad forms to the example's length. The rule is now an explicit
   count-then-cut step ("count your draft; delete the least build-critical
   until 5 remain"), and the discovery example itself was trimmed from 7
   to 5 questions so the anchor matches the cap.

3. Copy + language. Form copy anchors lead with the benefit ("Prefilled
   for you — send as is, or tweak anything first"); the localization rule
   demands native phrasing with a concrete wrong-vs-right anchor (快速确认,
   not the literal 快速简报); and forms now declare a top-level "lang"
   BCP-47 tag that the web card uses to render its own strings (Other
   chip, custom-answer copy, submit CTA) in the form's language instead of
   the UI locale — no more English "Other" chips inside a Chinese form.
   New i18n key qf.otherOption ships in all 19 locales.

Charter byte budget 15_872 → 16_896 across the two documented bumps, with
frozen-marker tests pinning the escape-hatch delegation, the hard cap, the
native-localization rule, and the lang anchor.

* fix(web): never surface a mid-stream question-form default

The streaming shaper reveals a question the moment its label parses, and
runs every question object — including the trailing, still-open one —
through mapRawQuestion. That let a MID-STREAM `default` reach the card:
the partial-JSON repair terminates it early, so "单位教育" surfaces as
"单位教" and ["历史背景与经过", "抗战精神与当代意义"] as ["历史背景与经过",
"抗战精"]. The card adopts a streamed default only while the answer is
still empty, so a truncated adoption freezes garbage that the completed
value can never overwrite — checkbox defaults (long arrays, near-certain
to be cut across delta chunks) effectively never prefilled, while short
radio scalars usually survived inside one chunk. Observed in production
run beaf2da0 (抗日主题汇报PPT): both maxSelections checkboxes rendered
with nothing selected despite the model emitting complete defaults.

Fix at the source: the trailing in-flight question exposes NO
defaultValue until its braces close. A closed object's braces are
balanced, so its default is complete; the card's empty-and-untouched
adoption rule then fills it exactly once, correctly.

Red spec reproduces the production body cut mid-default (went red on the
pre-fix tree) and pins that a closed question's default surfaces while
the next in-flight question's stays hidden.

* fix(prompts): align task-type form contract across all three copies; fit the slot menu inside the 5-question cap

Review follow-ups from PR #5573 (nettee):

- The od-default skill's task-type form now carries the same top-level
  "lang" tag as the daemon/contracts prompt copies, so host-rendered
  controls on that path key off the form language instead of falling back
  to the app locale. A new parity check reads all three copies and locks
  both contract keys ("lang" + taskType allowCustom: false) so the skill
  copy cannot drift again.

- The slim charter's default-shape recipe contradicted the new hard cap:
  "Between output and brand, in this order: platform, audience, tone.
  After brand: scale, then constraints" prescribed 7 questions in the
  same breath as "Hard cap: 5". The recipe is now 2 fixed slots plus a
  pick-AT-MOST-3 menu (same option vocabulary, same order), and the
  frozen-marker test asserts the menu wording is present while the old
  prescriptive sequence is gone — the charter can no longer carry both.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* feat(prompts): pin photo-overlay placement discipline in the slim charter

Real-imagery production value (the strongest-texture-lever rule) kept
shipping badges and caption cards that straddle the hero image's edge or
sit on the photo's focal subject — 2026-07-14 beta feedback on a
campus-open-day landing hero showed a session-type chip half-off the
photo, covering the graduates.

New Craft bullet: an overlay on a photo pins to ONE corner with a
consistent inset, sits fully inside the image bounds, stays clear of
faces/subjects, and reads on a legible surface with separating shadow;
with no safe corner it moves beside the image. Byte budget
16_896 -> 17_408 per the in-file bump trail; markers frozen.

* chore: retrigger ci

UI P0 (entry-settings) failed on 'Use with query carries the hydrated
starter prompt' — orthogonal to this PR's prompt-text diff, green on the
base branch's identical run, and passes locally on this branch (35s).
Empty commit to re-run the shard.

* chore: drop local-build churn in next-env.d.ts

A local 'next build' rewrote the generated routes.d.ts reference path and
the merge commit swept it in via add -A; restore the mainline version —
this PR's intent touches prompts/tests only.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>

* fix: address review findings — repeat-directive false positives, directions flag validation, router-form cap exemption

1. live-artifacts/render.ts: a data-od-repeat match now counts as a
   directive only inside an element's open tag; literal mentions in prose
   or comments render as inert content per html_template_v1 (red spec in
   tests/live-artifacts/render.test.ts).
2. cli.ts tools directions: --id/--label now fail fast on a missing
   value, a flag posing as the value, duplicates, or both flags at once
   (red spec in tests/directions-cli.test.ts) — agents call this straight
   from the prompt contract.
3. prompts/discovery.ts (+contracts mirror): the 5-question hard cap now
   names the verbatim six-field task-type router form as its one
   sanctioned exception, resolving the contradiction between two locked
   instructions; the cap itself is unchanged for authored/tailored forms.
   Marker frozen in discovery-form.test.ts; matrix snapshot regenerated.

* fix: second-round review — nested literal repeat false positive, keyboard-accessible Other chip

1. live-artifacts/render.ts: the nested-repeat guard now uses the same
   inside-open-tag detection as the top-level scan (extracted
   findRepeatDirective helper), so a literal data-od-repeat mention inside
   a repeated element's body renders inert instead of throwing; a REAL
   nested directive still throws. Red spec: reviewer's exact repro added,
   red on the previous head.
2. web QuestionForm Other chip: replaced the display:none checkbox-in-label
   with a real <button aria-pressed aria-expanded> plus focus-visible ring
   and UA-chrome reset, restoring Tab reachability for keyboard and
   screen-reader users. Existing chip tests migrated to the button role;
   new focus/toggle spec added.

* chore: retrigger ci

Web workspace tests failed on HomeView.community-filter-decouple
(aria-selected timing) — a main-side test untouched by this PR; the full
web suite passes locally on this exact head (4351/4351). Empty commit to
re-run the shard.

* fix: third-round review — media runs skip the slim design charter; token-aware repeat-directive scan

1. prompts/system.ts: resolve the media surface (image/video/audio via
   skillMode or metadata.kind) BEFORE building the prompt head, and gate
   the slim design charter on it — the media-generation contract is again
   the sole workflow authority on media runs, matching classic. The
   mid-conversation clarifying-questions block now keys off the charter's
   actual presence so media runs keep classic's clarify guidance. New spec
   proves image/video/audio slim prompts contain no discovery-form or
   HTML-handoff rules while design runs keep the charter.
2. live-artifacts/render.ts: findRepeatDirective is now quote- and
   comment-aware — a directive-looking string inside another attribute's
   quoted value or inside an HTML comment is authored text. Reviewer
   repros added as regressions (attribute-quoted literal, tag-like
   comment text); nested detection shares the same scanner.
Red-checked: 3 new specs fail with the two source files reverted.

* fix: fourth-round review — slim media head drops the Ask charter; comment-aware element-boundary scan

1. prompts/system.ts: the non-charter slim fallback head applied
   CHAT_MODE_OVERRIDE to every slim run that was not the design charter —
   including media runs, whose Ask charter ('do not create images/video/
   audio') contradicted the media contract. The head now branches
   explicitly: ask -> Ask charter, slim media (non-ask) -> classic skeleton
   (plain-stream API override + injection resistance) with the media
   contract as sole authority. Media spec extended to assert '# Ask mode'
   is absent.
2. live-artifacts/render.ts: extracted insideComment() and applied it to
   findElementEnd(), so same-tag tokens inside comments no longer move the
   depth counter (<!-- <div> --> threw 'unbalanced'; <!-- </div> --> closed
   the repeat early). findRepeatDirective shares the helper. Reviewer
   repros frozen as regressions.
Red-checked: both new specs fail with the two source files reverted.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: Mason <jinmeihong0201@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
2026-07-14 10:21:32 +00:00
lefarcenandGitHub da7c4ae051 fix(web): rank Community "Newest" by shipped publishedAt, not local install timestamps (#5597)
* fix(web): rank Community "Newest" by shipped publishedAt, not local install timestamps

The Community gallery Newest toggle sorted by the installed-record
updatedAt/installedAt. Those are stamped when a machine seeds its own
database, so a fresh install (or an upgrade that re-stamps the catalog)
writes the whole bundled catalog milliseconds apart in folder-walk
order — Newest showed reverse walk order (roughly reverse-alphabetical),
unrelated to publication recency.

Root-cause fix: bundled templates now ship a real publication date.

- packages/contracts: add optional manifest field publishedAt (ISO 8601).
- apps/web sortByNewest: freshness = publishedAt when parseable, else
  updatedAt (right signal for user-installed plugins); tie-breaks and
  stability unchanged.
- plugins/_official: backfill publishedAt into all 460 bundled manifests
  from each folder's first-appearance commit date in upstream git
  history.
- e2e: repository-resource guard requiring every bundled manifest to
  declare a parseable publishedAt, so the signal cannot silently erode.
- docs: field reference entries in plugins-spec (en + zh-CN).

Issue: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/ca0778af-97f5-4fc0-bc63-739280904979

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* worker: 模板用最新筛选,筛选出来的不是最新的

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): keep shipped appeal order inside same-publishedAt batches

Review follow-up (#5597, nettee): after the freshness tie on an equal
publishedAt, the comparator still consulted local updatedAt/installedAt,
which a fresh install stamps in folder-walk order — so every
same-publication batch (largest: 322 manifests on one catalog date)
re-sorted per machine instead of keeping the shipped visual-appeal
ranking. Same-publishedAt records now preserve their incoming position;
local timestamps break ties only when at least one side has no usable
publishedAt. Adds the requested regression spec pinning
same-publishedAt / different-updatedAt records.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): gate publishedAt freshness to bundled records; untrack oversized looper attachment

Review follow-up (#5597, nettee round 2): publishedAt was overriding
updatedAt for every visible plugin, so a user-installed github/local/
marketplace plugin carrying an old manifest publishedAt would be buried
behind bundled templates despite a newer local install. publishedAtMs
now returns null for non-bundled records — "Newest" means install
recency for user installs, catalog publication for bundled ones. Adds
the requested regression (non-bundled, old publishedAt, newer
updatedAt) and clarifies the field docs (en + zh-CN).

CI repair: commit 578f7a9 tracked .looper-attachments/bug13_1.png
(3.8MB bug screenshot), tripping the Static gate 1MiB blob guard and
the Validate workspace aggregate. Untrack it and gitignore
.looper-attachments/ so harness attachments stay out of the repo.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 08:43:57 +00:00
bc6a246c65 feat(slides): enhance deck project experience (#5083)
* feat(deck): enhance slide deck functionality with speaker notes and navigation improvements

- Added a switch for including speaker notes in slide decks, defaulting to true.
- Updated keyboard navigation to include 'R' for resetting to the first slide.
- Implemented click navigation for half-slide transitions.
- Enhanced the deck framework to support hidden programmatic prev/next controls.
- Improved handling of file inputs in question forms to support file submissions.

This update aims to improve user experience in creating and presenting slide decks, ensuring better accessibility and functionality.

* feat(artifacts): enforce semantic naming for user-facing deliverables

- Updated guidelines to require semantic filenames derived from user briefs for new artifacts, moving away from defaulting to `index.html`.
- Enhanced documentation to clarify naming conventions for HTML files, emphasizing the use of descriptive names like `investor-pitch-deck.html`.
- Adjusted prompts and hints across the system to reflect these changes, ensuring consistency in user-facing outputs.
- Improved test coverage to validate the new naming conventions and ensure proper implementation in various contexts.

This update aims to enhance clarity and usability in project file management, promoting better organization and user experience.

* feat(file-viewer): enhance speaker notes display and improve file workspace functionality

- Updated the speaker notes section to include a styled placeholder for empty notes, improving user experience.
- Changed the icon in the file workspace from "slides" to "panel-left" for better clarity.
- Introduced new sub-category filtering options in the file workspace, enhancing organization and navigation.
- Improved styling for the page creator search input and added visual feedback for loading states.

These changes aim to enhance usability and visual consistency across the file viewer and workspace components.

* test(web): align workspace tests with pages switcher

* test(e2e): open design files through pages menu

* fix(deck): keep source mode reachable

* fix(workspace): restore uploaded file tabs

* feat(deck-thumbnail): implement virtualized thumbnail rail for improved performance

- Introduced a new `DeckThumbnailRail` component to optimize the rendering of deck thumbnails, reducing memory usage and improving load times.
- Enhanced the `FileViewer` to integrate the new thumbnail rail, allowing for lazy loading of thumbnails based on viewport visibility.
- Added tests for the `DeckThumbnailRail` to ensure correct functionality and caching behavior.
- Updated styles for better visual consistency and performance during thumbnail rendering.

These changes aim to enhance user experience by providing a smoother and more efficient way to navigate through deck slides.

* feat(deck): enhance deck rendering with thumbnail support and keyboard shortcuts

- Integrated `DeckSlideThumbnail` for rendering individual slides in a shadow root, improving performance and reducing main-thread load.
- Updated `DeckThumbnailRail` to utilize the new thumbnail component, allowing for efficient slide navigation.
- Implemented keyboard shortcuts for deck navigation, enhancing user experience during presentations.
- Added tests for `buildDeckRenderInput` to ensure proper functionality of the deck stage fallback.
- Improved styling for loading states in the viewer, providing a smoother visual experience.

These changes aim to optimize the deck viewing experience by reducing resource consumption and enhancing interactivity.

* feat(deck): improve thumbnail loading and speaker notes editing experience

- Refactored `DeckSlideThumbnail` to enhance loading states with a new shimmer effect, improving visual feedback during thumbnail rendering.
- Updated `DeckThumbnailRail` to manage loading states for thumbnails, ensuring a smoother user experience.
- Simplified the speaker notes editing interface by removing the toggle switch, allowing direct editing from the notes body.
- Enhanced accessibility by adding roles and focus states to speaker notes elements.
- Added tests to verify the new loading states and speaker notes functionality.

These changes aim to optimize the user experience in deck navigation and speaker notes management, providing a more intuitive and responsive interface.

* feat(file-viewer): enhance version export functionality and speaker notes interaction

- Introduced new export options for file versions, including PDF and image formats, improving the ability to download and share content.
- Implemented a streamlined process for exporting version content, with progress indicators and error handling for a better user experience.
- Enhanced speaker notes editing by refining event handling to prevent unintended focus shifts, ensuring a smoother editing experience.
- Updated styles for improved visual consistency in the file version download interface.
- Added tests to verify the functionality of version exports and speaker notes interactions.

These changes aim to optimize the file viewer's usability and enhance the overall experience when managing and exporting project files.

* fix(web): polish export menus and creator filters

* fix(file-viewer): fall back to preview exports

* test(file-viewer): cover version export fallbacks

* test(file-viewer): assert loading skeleton by role

* fix(file-viewer): restore html source mode affordance

* fix(file-viewer): capture full version fallback snapshots

* docs: trim slides handoff eof

* test(exports): cover full preview snapshot requests

* test(file-viewer): account for snapshot options arg

* refactor(file-viewer, file-workspace): streamline export functions and enhance page creator dialog visibility

* test(file-workspace): enhance slide template creation and mock plugin responses

* feat(export): add support for versioned exports and enhance HTML export functionality

* feat(export): implement versioned export support across multiple formats and enhance related tests

* feat(file-viewer): enhance manual edit functionality with commit sequence tracking and add test for inline text session saving

* style(speaker-notes): update filmstrip and thumb-frame styles for improved responsiveness and add corresponding tests

* feat(file-viewer): enhance comment composer functionality with portal metrics and improve draw toolbar layout for mobile views

* feat(analytics): track slide deck viewing experience

Add analytics for the new deck/slides experience introduced in this PR:
the deck viewer surface, in-deck slide navigation, thumbnail rail, and
speaker-notes authoring. Presentation entry stays on the existing
present_popover click to avoid double-counting.

- contracts: DeckViewerSurfaceViewProps (surface_view),
  DeckViewerClickProps (ui_click), SpeakerNotesSaveResultProps
  (new speaker_notes_save_result event), wired into the event unions.
- web: track* wrappers + fire helpers in FileViewer — deck_viewer
  surface_view on deck mount, slide_prev/next/reset, thumbnail_select,
  thumbnail_rail_toggle (expand/collapse), speaker_notes_edit, and
  speaker_notes_save_result (preview + presenter surfaces).
- tests: deck analytics suite asserting the emitted (event, props) pairs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(file-viewer, file-workspace): enhance export toast functionality and improve localization for page creation

* feat(home-view, plugins-home-section, agent-skill): update default facet preference, enhance category selection handling, and improve localization support

- Set `preferDefaultFacet` to true in HomeView component for better user experience.
- Introduced `handlePickCategory` function in PluginsHomeSection to manage category selection based on card layout visibility.
- Updated PRIMARY_CATEGORIES to reflect the new order and added localization for titles and descriptions in adaptAgentSkill.
- Enhanced tests to verify the correct handling of display copy and tags from SKILL frontmatter.

* feat(plugins-home, file-workspace): introduce commercial category labels and enhance deck handling

- Added a new module for resolving and localizing commercial category labels for plugins.
- Updated the FileWorkspace and PluginCard components to display the commercial category label on deck cards.
- Enhanced the PageCreatorPresetFrame to adjust frame classes based on deck presets.
- Refactored subcategory handling in facets to align with the new commercial category taxonomy.
- Improved localization support for new category labels across multiple languages.

* feat(deck-preview): implement deck preview scaling and enhance home hero layout

- Introduced `useDeckPreviewScale` hook to manage scaling of deck previews within a fixed 1280×720 logical viewport, ensuring proportional rendering across various templates.
- Updated `FileWorkspace`, `HomeHero`, and `PluginCard` components to utilize the new scaling functionality for improved visual consistency.
- Enhanced the layout of the home hero section to accommodate new subtype chips and descriptions, including a "More" dropdown for overflow handling.
- Improved localization support for new UI elements across multiple languages.

* fix(PreviewModal): clarify sidebar prop documentation and update test description

- Updated the documentation for the `PreviewSidebar` interface to specify that the sidebar's stage-edge handles are not shown when the sidebar prop is absent.
- Modified the test suite description for `PreviewModal` to be more concise and relevant.

* test(e2e): select prototype visual card

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:41:47 +00:00
323246a64c Website Clone: first-party Home entry, fidelity, real-clone example cards + tracking (#5178)
* Add Website Clone home entry with bundled web-clone skill and root-relative asset preview fix

* Retrigger CI

* Open the site entry after website-clone turns

* Harden web-clone skill asset and color fidelity

Recon now scrolls the full page and captures @font-face rules, real font/image
resource URLs, computed section palette, and resolved root variables. Asset
harvest is rebuilt on the browser network stack so cross-origin CDN images,
fonts, and media (hotlink-protected included) all land locally, with a
self-hosted fonts.css and an originalUrl→localPath manifest. Clone audit gains
--recon --strict fidelity gates (fonts self-hosted, images downloaded, key
section colors exact) that exit 2 on violations, and SKILL.md makes all three
a mandatory delivery gate.

* Default web-clone mode to faithful reproduction when unconfirmed

A real nike.com run showed the agent silently downgrading to visual-homage
mode (placeholder brand, typography blocks instead of photography, approximated
palette) because the user skipped the turn-1 confirmations and the site was a
copyright-heavy commercial SPA. Fidelity is the point of this scenario: default
to faithful reproduction, land real assets first, record trademark/copyright
replacements as a pre-deploy checklist for the user to decide in Step 6, and
never skip script recon just because the target looks bot-sensitive.

* Streamline web-clone runs: skip discovery with URL, exempt brand constraints, actionable playwright setup

Three run-path optimizations for the Website Clone scenario:

1. POST /api/projects derives metadata.skipDiscoveryBrief for web-clone
   projects whose pending prompt already carries the target URL — the URL is
   the brief, so the turn-1 question form no longer stalls the run (an
   explicit client flag still wins). Covered by
   project-create-web-clone-discovery.test.ts.
2. Prompt composition skips the design-system authoritative block and craft
   rules for intent=web-clone runs: the fidelity target is the original
   site, not the project brand; brand application belongs to the skill's
   Step 6 replacement phase.
3. The plugin's playwright-loader now also resolves from the process cwd
   (so 'npm i -D playwright' in the project root works for scripts staged
   under .od-skills/) and fails with exact fix commands; SKILL.md documents
   the staged-path resolution and forbids downgrading to eyeballing because
   of missing deps.

* Swap the copyright guardrail for faithful reproduction on web-clone runs

The official designer prompt's 'don't recreate copyrighted designs — help
the user build something original instead' bullet made web-clone agents
silently rebrand clones (Nike → placeholder brand, original SVGs instead of
real photography) even after the skill mandated faithful defaults: a system
prompt guardrail outranks skill blocks. For intent=web-clone runs the bullet
is swapped for a faithful-reproduction rule that routes trademark/copyright
caution through a pre-deploy replacement checklist (NOTES.md) the user owns.
Prompt tests guard both directions of the substitution.

Also: recon now captures scroll-feel signals (scroll-behavior, scroll-snap
rule count, sticky/fixed census) and SKILL.md makes matching the original's
scroll behavior (smooth-scroll libs, snap, sticky) a fidelity requirement.

* Extract web-clone into a shared global skill referenced by ref

The Website Clone workflow (SKILL.md + 14 recon/harvest/audit scripts +
references) moves from the example-web-clone plugin into the global skill
registry at skills/web-clone/. example-web-clone now references it via
od.context.skills:[{ref:'web-clone'}] instead of shipping its own copy, so the
upcoming per-site clone example plugins can all share one skill rather than
each duplicating a 16KB SKILL.md and its scripts.

Pinned-plugin skill resolution (server.ts) previously only loaded a plugin's
LOCAL ./SKILL.md; a {ref} skill loaded nothing, so the body never entered the
prompt and the scripts never staged. It now falls back to resolving the ref
against the global registry, loading the body and registering the skill dir for
cwd staging. Verified end-to-end in a live run: skills/web-clone stages to
.od-skills/web-clone-<hash>/ with SKILL.md + all scripts.

guard.ts residual-JS allowlist follows the scripts to their new home; a
structural test locks the manifest<->skill contract.

* Add 7 Website Clone example-prompt cards for the Home clone rail

nexu.io, open-design.ai, anthropic.com, vercel.com, resend.com, linear.app,
raycast.com — each a thin example plugin that references the shared web-clone
global skill (od.context.skills:[{ref:'web-clone'}]) so none duplicate the
workflow. Each seeds the composer with a faithful-clone prompt for its URL and
ships a self-contained example.html preview built from a real recon capture of
the target homepage, so the gallery card shows what the card clones. They
surface under the Website clone chip's example-prompt rail (verified in a live
desktop run) and inherit the chip's intent:'web-clone' metadata, so clicking one
runs through the full fidelity path (skip discovery, brand exemption, faithful
reproduction, script recon/harvest/audit).

* Polish clone example-card previews with a framed hero capture

The first pass inlined a downscaled full-page screenshot cropped mid-content —
it read as a raw screenshot dump. Each preview is now a crisp above-the-fold
hero capture (1440x900 DPR2, 16:10) presented inside a browser-window chrome
(traffic lights + host URL pill) on a brand-tinted gradient, so the gallery
card reads as an intentional site preview. Verified by rendering the previews
headless.

* Cache the plugin list so Home remounts don't grey the create rail

HomeView re-fetched + parsed the large /api/plugins payload on every remount,
leaving the create-rail chips disabled for 1-2s each time. A module-level cache
(listPluginsFresh, 10s TTL) serves warm mounts without the network round trip so
pluginsLoading clears within a frame; the first render stays cheap (empty list)
to keep the click off the critical path. Also hide the base example-web-clone
plugin from the example rail (the per-site clone-* cards are the real examples).

* Ship real self-contained clones as example-card previews + align seed prompt

Each clone-* example.html is now a real, self-contained reproduction of its
target site (inlined CSS/fonts/images via document.styleSheets serialization so
CSS-in-JS/Next.js SPAs like Vercel/Raycast capture correctly, oversized icon
sprites dropped, rasters downscaled to stay under the 5MB preview cap, plus a
scroll-reveal motion layer). Preview renders the real HTML; Remix copies it into
a project as index.html. Manifests reuse the base web-clone useCase.query with a
{{targetUrl}} default per site, so picking a card seeds the same preset prompt
with the URL filled in instead of the marketing description.

* Shrink clone example previews ~33% (dedup + drop EOT + WebP)

Three lossless-to-near-lossless passes on the self-contained clones:
- Dedup repeated data-URIs (same image used in <img> + css url() N times): store
  each once as a :root CSS var, point url() at var(), hydrate img[src] from the
  var via a tiny script. Resend alone shed 1.8MB.
- Drop leftover EOT/fontobject font data-URIs (IE-only, redundant with woff2).
- Re-encode inlined raster images to WebP (~50% smaller than JPEG at equivalent
  quality) via cwebp q72.
Total across the 7 cards: ~23MB → ~15.5MB, every card now under 3MB (was up to
5MB). Image hydration + fidelity verified by headless render.

* Lead create rail with Website clone; seed example cards with just the URL

- Move 'web-clone' to the front of CREATE_RAIL_ORDER so Website clone is the
  first create-rail card, ahead of Slides (deck).
- Website-clone example cards (Clone Nexu, Clone Vercel, …) now seed the
  composer with the same short 'clone this site: <url>' scaffold the chip uses
  (localized webClonePromptSeed prefix + the card's targetUrl), e.g.
  'Website URL to clone: https://nexu.io', instead of the multi-paragraph
  build spec. Picking the chip with an empty composer already seeds the bare
  prefix for the user to paste a URL after.
- Fix a test-only regression from the Home plugin-list cache: the module-level
  visible-plugins cache leaked a case's mocked /api/plugins payload into later
  cases via listPluginsFresh, greying the rail / stranding scenarios on
  'Template None'. Add resetPluginsCache() and clear it in a global afterEach.

* Track web-clone example-card picks (example_prompt / example_open_project)

The Website-clone example cards (Clone Nexu / Vercel / …) had no analytics on
pick or remix, so the site-clone funnel could not be broken down per example.
Wire the two clicks the contracts already type (埋点文档 row 116):

- useExamplePlugin → ui_click area=chat_composer element=example_prompt
- duplicateExamplePlugin (Remix) → element=example_open_project

Both carry chip_id (web-clone) + plugin_id (example-clone-<site>) + plugin_type
for per-site breakdown. The created project's own project_kind=web_clone (already
derived from metadata.intent via projectKindToTracking) still rides
project_create_result / run_created, so 'how many projects are website clones' is
answerable without extra work. No contracts change — the element enum and the
web_clone project_kind already existed. Adds a behavioral test asserting both
events fire with the right triplet + attribution.

* Mirror web-clone copyright-guardrail swap into contracts/BYOK prompt

PR #5178 blocking review (nettee/looper): the daemon prompt special-cases
intent='web-clone' (faithful-reproduction rule) but the contracts prompt stack —
which apps/web uses for API/BYOK runs — imported OFFICIAL_DESIGNER_PROMPT directly
and never routed web-clone, so BYOK website-clone runs kept the original 'build
something original' guardrail. Daemon and BYOK now behave identically:
- contracts/prompts/official-system.ts: add COPYRIGHT_GUARDRAIL_BULLET,
  WEB_CLONE_COPYRIGHT_GUARDRAIL_BULLET, renderOfficialDesignerPrompt (mirrors the
  daemon renderer; contracts base has no execution placeholders so it only swaps
  the bullet).
- contracts/prompts/system.ts: composeSystemPrompt routes the charter through
  renderOfficialDesignerPrompt({ webCloneFidelity: metadata.intent==='web-clone' }).
- Add system-prompt-web-clone.test.ts covering both modes + the byte-identical
  bullet guard.

* Complete web-clone BYOK prompt parity + rebase relative JS asset refs

Two remaining blocking issues from PR #5178 looper re-review:

1. contracts composeSystemPrompt still appended the active-design-system
   'authoritative' block + visual-direction override unconditionally, while the
   daemon suppresses both for intent==='web-clone'. A BYOK web-clone run with an
   active design system was still told the design system wins, fighting faithful
   reproduction. Gate both sections behind !isWebCloneRun; add contracts tests
   asserting web-clone BYOK prompts drop the design-system guidance.

2. rewriteInlinedScriptAssetRefs only handled root-relative literals. When
   js/app.js is inlined into the HTML entry, a sibling lookup like
   fetch('./data.json') / new Worker('./worker.js') / import('./chunk.js')
   resolved against the HTML root instead of js/. Take the source script path and
   rebase confirmed relative refs against it (mirrors the CSS pass), membership-
   confirmed; add unit cases for ./sibling, ../parent, and escaping/missing.

* Website-clone examples: plain text prompt cards, drop embedded HTML + Remix

The 7 site example cards embedded a full self-contained clone of each target site
(anthropic.com, vercel.com, …) as example.html and offered a one-click Remix that
copied that markup into a project. Shipping verbatim copies of other companies'
sites is an IP risk, so replace them with plain text prompt cards:

- Delete plugins/_official/examples/clone-* (7 dirs, incl. the example.html
  reproductions and the Remix-enabling preview.entry).
- Add a 'web-clone' entry to HOME_PROMPT_EXAMPLES (en + zh-CN, others fall back to
  en): 7 text cards that fill the composer with 'Website URL to clone: <url>'.
  With the site plugins gone, the web-clone chip's preset rail is empty so these
  static text cards render instead. Clicking one fills the URL prompt and (via
  the existing usePromptExample) still fires ui_click element=example_prompt
  chip_id=web-clone — no plugin_id / no Remix / no example_open_project.
- Remove the now-dead webCloneCardTargetUrl seed override; refresh stale comments.
- Rewrite HomeView.web-clone-tracking test for the text-card path (asserts text
  cards render, no plugin-preset / no remix affordance, and the pick still tracks).

The base example-web-clone skill + web-clone run behavior (intent, fidelity
prompt, discovery skip) are unchanged.

* Give web-clone example cards a site logo + domain, not raw prompt text

The 7 Website-clone examples rendered as near-identical white boxes each showing
'Website URL to clone: https://<site>' — visually flat and hard to scan. Render
them as a logo gallery instead: each card shows the site's own favicon (resolved
at render time via a public favicon service — no third-party brand assets are
bundled) in a rounded tile above the bare domain (nexu.io, vercel.com, …). A
blocked/failed icon fetch falls back to a lettered monogram tile. Click behavior
is unchanged: the full 'Website URL to clone: <url>' prompt is still what fills
the composer and fires ui_click element=example_prompt. Non-URL prompt examples
(other chips) keep the plain text card.

* Web-clone examples: keep only open-design, add kimi

Legal review flagged brand-copy risk in the example rail. Drop the
nexu / anthropic / vercel / resend / linear / raycast site prompts and
keep just open-design.ai plus kimi.com (official Kimi partnership).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 03:28:06 +00:00
e02a36f51c feat(examples): add Open Design homepage — interactive 3D showcase (#5355)
* feat(examples): add Open Design homepage — interactive 3D showcase

Adds the live open-design.ai homepage as a first-party, self-contained
example: an interactive React Three Fiber / Next.js hero with a real-time
3D "Open Design" wordmark, sticker collage, variable fonts, cursor
parallax, and scroll-driven motion. Renders in the sandboxed powered
preview.

Made repo- and preview-safe without touching the app's runtime:
- 3D geometry Draco-compressed (KHR_draco_mesh_compression): hero wordmark
  4.4MB -> 300KB, cnt 2.2MB -> 548KB, decoded by the bundled DRACOLoader.
  Visually identical (verified).
- Work/sticker imagery converted to LOSSLESS WebP: pixel-identical to the
  source PNGs (alpha + visible-pixel diff = 0), 12.2MB -> 7.1MB.
- Every file now < 1MB, so the template is fully self-contained in-repo
  with no external asset host.
- Path-portable so it renders from the nested preview subpath, not just
  the site root: absolute asset paths made relative; font/bgm URLs derived
  from document.baseURI instead of window.location.origin; App-Router
  createHrefFromUrl pinned to "/" and history push/replaceState URL
  neutralized so hydration matches and baseURI stays put (fixes the
  React #418 + broken relative-asset resolution seen when serving a
  Next RSC bundle from a subpath).

Verified end-to-end from a deep subpath: 3 Draco models + 41 WebP + 3
fonts load, zero 404s, zero console errors, no hydration error,
interactive (scroll/cursor).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(examples): list the full asset set in open-design homepage manifest

Addresses review: od.context.assets previously listed only the design
assets (models, fonts, imagery, bgm) and omitted the Next runtime chunks
under assets/_next/. Applying the plugin would stage an incomplete copy
of the static export, missing the runtime files example.html needs to
render. Now lists every file the entry requires (example.html + all 54
files under assets/, including the _next chunks + css). Also cleaned up
the truncated od.useCase.query text into a complete sentence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(examples): complete the open-design homepage asset closure

Addresses the review's local-asset-closure blocker (three dangling
references the mirror never captured):
- Restore the Next polyfills chunk assets/_next/.../a6dad97d9634a72d.js
  (referenced by the noModule fallback script; modern browsers skip it,
  so it never 404'd at runtime but left the closure incomplete).
- The work grid's passcode-protected /2026 tile pointed at
  work/tt01|tt02 images that were never in any mirror source (they 404
  on the live site too). Add lockscreen placeholder images in the site
  palette so the reference resolves and — importantly — the client work
  data still matches the server-rendered HTML (removing the tile instead
  reintroduced a React #418 text hydration mismatch).
- od.context.assets now lists the full closure (58 entries).

Also corrected the manifest/SKILL 'zero external dependencies' wording:
the Draco decoder is fetched from Google's gstatic CDN on demand (same
pattern as the other WebGL examples); everything else is bundled locally.

Re-verified from a preview subpath: 3 Draco models + fonts + imagery
load, zero local 404s, zero console errors, no #418.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(examples): remove third-party network calls + committed key from homepage bundle

Addresses review: the bundled preview shipped two undisclosed external
requests, contradicting the manifest/SKILL 'only Draco loads externally'.

- useWeather() made a live QWeather request with a hardcoded API key
  (devapi.qweather.com/...&key=...) to append a live temperature to the
  clock HUD. Replaced the fetch with static data ({now:{temp:"19"}}) —
  no network call, key removed from the bundle. The HUD still shows a
  temperature (static, as befits a mirror showcase).
- Removed the Vercel toolbar injection IIFE (vercel.live/_next-live/
  feedback/feedback.js + a data-deployment-id). It was cookie-gated to
  Vercel team members so it never fired in preview, but it left a
  committed external endpoint in a first-party plugin.

Re-verified from a preview subpath: the ONLY external request is now the
gstatic Draco decoder (as the manifest states); 3 models + fonts +
imagery load, zero local 404s, zero console errors, no #418.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:42:32 +00:00
49d64f0e8c fix(examples): resize WebGL canvases lazily in the render loop (#5327)
The powered-preview iframe can finish loading while its viewport is
still 0x0 (hidden/animating dialog). Every WebGL example sized its
canvas once at init plus on window resize, so when no resize event is
delivered after the dialog settles, the drawing buffer stays 0x0 and
the preview renders black until something (e.g. opening the plugin-info
panel) changes the iframe width.

Guard each resize handler to no-op when dimensions are unchanged and
call it from the render loop, so the canvas self-heals within one frame
of the iframe getting its real size — no reliance on resize events.
Applies to all 15 webgl-* examples.

Reported by QA: preview stays black until fullscreen/plugin-info is
toggled.

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 06:52:57 +00:00
7d005a17c3 feat(examples): add 10 WebGL example templates (#5312)
* feat(examples): add 10 WebGL example templates

Adds 10 first-party powered-preview WebGL examples under plugins/_official/examples,
each a self-contained dir (SKILL.md + example.html + open-design.json, self-contained i18n):

C-tier — faithful Codrops ports (MIT + credit + LICENSE; original imagery):
  webgl-depth-gallery, webgl-pixel-reveal-gallery,
  webgl-horizontal-parallax, webgl-distortion-grain
D-tier — original self-contained WebGL2 shader heroes (no third-party assets):
  webgl-caustic-pool, webgl-halftone-drift, webgl-holographic-foil,
  webgl-voronoi-cells, webgl-aurora-veil, webgl-liquid-iridescence

All changed files are <1MB (pass the blob-size gate). The two large mirror
showcases (oryzo, opendesign) are deferred: they exceed the 1MB blob guard and
need their heavy assets moved to repository-assets R2 (per the gate's guidance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(examples): stop shader setup after WebGL2 fallback message

Wrap each pure-shader example script in an IIFE and return early when
getContext('webgl2') is unavailable, so unsupported environments settle
on the fallback message instead of throwing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:29:38 +00:00
Tom HuangandGitHub 956b6607f5 feat(preview): add powered WebGL previews and examples (#5266)
* feat(powered-preview): implement cross-origin isolation for WebGL and Worker artifacts

- Introduced a new powered preview feature that allows rendering HTML artifacts in a cross-origin-isolated iframe, enabling the use of Web Workers, WASM, and SharedArrayBuffer.
- Added API endpoint `/api/preview/isolation` to report the daemon's directly-reachable HTTP origin for rendering.
- Implemented logic in the file viewer to determine if an artifact requires powered preview based on its content.
- Updated the file serving routes to support powered preview, ensuring proper headers are set for cross-origin isolation.
- Enhanced the user interface to include new scenarios for WebGL and Worker visualizations in the home hero section, with corresponding translations and descriptions.

This feature addresses the limitations of the opaque-origin preview sandbox, allowing for more complex GPU and compute applications to function correctly.

* feat(preview): add WebGL templates and lazy HTML previews

* test(web): stabilize project back routing assertion

* fix(preview): address powered preview review blockers

* fix(preview): isolate powered preview origin
2026-07-08 08:58:51 +00:00
390fcf88f3 fix(plugin-previews): keep Community gallery previews in sync with shipped plugins (#5262)
* fix(plugin-previews): validate baked preview metadata against shipped plugins

The Home Community gallery renders whatever data/plugin-previews/manifest.json
names, and nothing kept that metadata in sync with reality: #4815 deleted 19
example plugins but left all 19 manifest entries behind, #4040 shipped a card
showing another plugin's imagery, a failed re-bake silently leaves the old
clip serving outdated content, and durationMs recorded the intended walk time
rather than the encoded file (23 entries carried holdMs > real duration, so
the card's idle loop pointed past the end of the clip).

- add scripts/check-plugin-preview-manifest.ts to pnpm guard: every manifest
  entry must name a shipped plugin, own its clip keys (<id>/<hash>/... or the
  legacy <id>.<hash>....), carry a coherent fingerprint, and never share a
  clip with another entry; prune the 19 orphaned entries it caught
- bake validation (BAKE_VERSION 5): reject blank clips (whole-clip luma range
  via ffprobe signalstats), record durationMs from the encoded file, clamp
  holdMs to it, and write a bake-report.json naming skipped/stale/blank
  plugins instead of swallowing the failure
- strict mode (--strict / PREVIEW_STRICT=1) fails the run when a bake left
  broken metadata behind; the pre-merge validation workflow now runs strict
  while the post-merge/nightly publishers stay lenient

* fix(plugins): make the hyperframes example self-contained

The example loaded cdn.tailwindcss.com (a render-blocking head script the
page never used — every style is inline) plus Google Fonts. Inside the
sandboxed gallery iframe or on a slow network the stalled script showed a
long blank instead of the composition, and the CI bake raced the webfonts.
Drop both: the page now renders from local system font stacks with zero
external requests, so the live preview, the detail iframe, and the baked
clip all show the same content.

* fix(plugin-previews): address review — fail fast on probe errors, widen strict trigger paths

- probeClipMs/clipLumaRange now throw instead of returning null: ffprobe is
  required validation infrastructure, and a swallowed probe error silently
  disabled exactly the checks this PR adds. A thrown error becomes an
  'error …' skip, is recorded in bake-report.json under 'errors', and fails
  strict mode — while the routine skips every sweep has (non-html plugins
  404ing the preview route) still never trip strict.
- bake-plugin-previews-pr.yml now also triggers on plugins/community/**,
  .github/actions/bake-previews/**, and itself, so community-plugin preview
  changes and bake-recipe changes cannot merge without the strict pre-merge
  validation.

* fix(plugin-previews): bake static pages as a held still instead of skipping forever

The strict pre-merge bake on this PR caught five manifest entries that could
NEVER refresh: example-dating-web, example-html-ppt-zhangzara-8-bit-orbit,
example-frame-logo-outro, od-new-generation, example-orbit-notion. All five
are (near-)static pages — the CDP screencast only delivers frames when
something repaints, so they produce 1-4 frames and the old 'frames < 5' guard
skipped them on every bake, nightly included, leaving their committed entries
permanently stale (their old clips are the 103-210ms degenerate files in the
manifest today).

Encode <5-frame captures as a still instead: hold the last frame for the
idle-loop span (half before the trailing repeated concat entry, half
inherited by it, summing to ~HOLD_MS). Only a zero-frame capture is a real
failure now. The blank-luma check still rejects stills of nothing.

* fix(plugin-previews): never persist a manifest entry without a fingerprint

When the preview fingerprint fetch failed, the loop still rendered and — on
an otherwise successful bake — persisted an entry with hash: null and
un-fingerprinted keys: metadata the content-hash reuse skip can never match
again and the new manifest guard rejects. Treat a missing fingerprint as an
infrastructure error instead: skip the plugin, record it under errors in
bake-report.json (strict mode exits non-zero), and leave any committed entry
untouched for the next sweep.

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
2026-07-08 03:44:05 +00:00
HinawandGitHub 02c22711c1 Add Design System Source Context plugin (#4931) 2026-07-03 03:50:37 +00:00
ACATandGitHub cb4d4efbd2 Add Glass Design System UI Kit plugin (#4395)
* Add G2 design system UI Kit plugin

* Fix G2 UI kit packaged references

* Trigger workspace validation

* Prevent G2 input preview submit
2026-07-03 03:04:10 +00:00
mac.alviandGitHub 1799d33e1b Add Agent Skill: Principal UI/UX Architect & Motion Choreographer (Awwwards-Tier) plugin (#4800) 2026-07-03 03:03:42 +00:00
Tom HuangGitHubopen-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>Amy
6be4e56544 feat(workspace): add plan mode and Excalidraw sketch flows (#4862)
* feat(daemon): add 'plan' session mode and update related functionality

- Introduced a new session mode 'plan' alongside existing 'design' and 'chat' modes, allowing for editable document creation.
- Updated various functions and interfaces to accommodate the new session mode, including normalization and usage in commands.
- Enhanced markdown rendering capabilities by integrating micromark and its GFM extension for improved markdown processing.
- Added new input types for question forms, expanding the range of user interactions.
- Updated UI components to reflect changes in session modes and ensure proper handling of next steps based on the current mode.

* feat(chat): integrate SessionModeToggle into ChatComposer and HomeHero

- Added SessionModeToggle component to both ChatComposer and HomeHero for improved session management.
- Updated HomeHero styles to accommodate the new mode switcher layout.
- Adjusted countdown timer in QuestionsPanel to extend the skip countdown from 120 seconds to 10 minutes, enhancing user experience.
- Added tests to ensure the countdown functionality works as expected.

* feat(FileViewer): implement synchronized scrolling for Markdown editor and preview

- Added functionality to synchronize scrolling between the Markdown editor and preview panes.
- Introduced new types and utility functions for managing scroll behavior.
- Enhanced the MarkdownViewer component to handle scroll events and maintain scroll position across different modes.
- Updated the component's state management to accommodate the new scrolling features.

* feat(excalidraw): integrate Excalidraw into the project

- Added @excalidraw/excalidraw as a dependency in package.json.
- Updated vitest configuration to include an alias for Excalidraw.
- Imported Excalidraw CSS in the layout component for styling.
- Modified AssistantMessage component to handle optional projectId.
- Enhanced FileOpsSummary to display delete operations.
- Implemented new Excalidraw scene management in SketchEditor and FileWorkspace components.
- Updated MarkdownViewer to support file mentions and improved file handling.
- Refactored various components to accommodate Excalidraw integration and ensure compatibility with existing features.

* feat(files): improve sketch and file handoff flows

* test: align post-merge expectations

* chore(nix): refresh pnpm deps hash

* fix(workspace): stabilize sketch persistence and ci checks

* fix(workspace): address review blockers in editable files

* fix(workspace): persist cleared sketch scenes

* test(workspace): type sketch editor mock scene

* fix(workspace): serialize sketch autosaves

* fix(workspace): keep sketch save revisions current

* test(e2e): stabilize project workspace smoke flows

* fix(analytics): preserve plan mode for BYOK runs

* test(e2e): stabilize new project rail interactions

* fix(files): stop bash delete parsing at shell operators

* test(e2e): stabilize ui cold-start suites

* fix(viewer): preserve absolute markdown image sources

* feat(workspace): preload sketches and enhance markdown save options

- Added functionality to preload persisted sketches before opening the tab.
- Introduced new MarkdownSaveOptions type to manage save behavior.
- Updated saveMarkdownText to handle options for refreshing files and showing saving state.
- Enhanced FileViewer to maintain focus and selection during metadata refresh.
- Implemented a Toast component for user feedback on save and export actions.

* fix(web): stabilize markdown and sketch editor polish

* fix(web): finish sketch editor merge resolution

* chore(nix): refresh pnpm deps hash

* fix(plan): bypass discovery and stabilize markdown sync

* fix(web): simplify scene retrieval in SketchEditor component

* feat(web): enhance markdown viewer with auto-save functionality

- Implemented passive auto-save status in the MarkdownViewer component, replacing the manual Save button with an auto-save indicator.
- Introduced new hooks and state management for tracking auto-save events and displaying the last saved time.
- Added support for synchronized scrolling between the markdown editor and preview.
- Created a new markdown-scroll-sync module to handle scroll synchronization logic.
- Updated localization files to include new strings for auto-save messages.
- Added a SketchEnginePrewarm component to optimize Excalidraw loading times.

* feat(web): enhance session mode toggle with cost indicators

- Added cost tiers for each session mode in the SessionModeToggle component, providing users with a visual representation of usage costs.
- Introduced a new ModeCostTag component to display cost information alongside session mode labels.
- Updated localization files to include new keys for cost labels and notes.
- Enhanced styling for cost indicators to improve user experience and clarity.
- Refactored EntryShell to open the new project modal instead of creating a blank project directly from the rail.
- Implemented a utility function in markdown-scroll-sync to check for vertical progression in block offsets.

* feat(web): add max height adjustment for session mode description card

- Introduced maxHeight prop to the ModeDescriptionCard component to control the height of the description card based on available space.
- Implemented useLayoutEffect in SessionModeToggle to dynamically calculate and set the maximum height of the description card, ensuring it does not overlap with the project tab bar.
- Updated tests to verify that session modes display their expected usage/cost correctly in the UI.
- Enhanced localization files to include new cost-related strings for various languages.

* feat(web): implement goBack function for improved navigation and update auto-open logic

- Added a new `goBack` function to handle in-app navigation, allowing users to return to the previous route instead of a hardcoded destination.
- Updated the `navigate` function to maintain history state for better back navigation.
- Refactored auto-open logic to prioritize produced artifacts, allowing markdown files to be opened alongside HTML files.
- Updated tests to cover new navigation behavior and artifact selection logic.
- Enhanced localization files to include new descriptions for workspace actions.

* refactor(web): remove create design system functionality and update design files panel actions

- Removed the `onCreateDesignSystem` prop and associated button from the DesignFilesPanel component.
- Updated the empty state actions to include a button for creating a new document via the `onPaste` function.
- Adjusted tests to reflect the removal of the design system creation action and ensure the new document button is functional.
- Enhanced the MarkdownViewer component by adding a placeholder for the text area and removing the header bar for a cleaner interface.
- Updated localization files to include a new placeholder string for the markdown editor.

* fix(web): restore markdown placeholder translations

* refactor(web): streamline MarkdownViewer and enhance localization

- Removed unnecessary state management and reload functionality from the MarkdownViewer component for improved performance.
- Added a placeholder text for the markdown editor in multiple localization files to enhance user guidance.
- Updated styles for the save state indicator in the viewer to improve visual clarity and alignment.
- Adjusted tests to reflect changes in the MarkdownViewer and ensure proper functionality.

* fix(web): adjust SketchEditor button size and remove shortcut hints

- Reduced the icon size in the SketchEditor component from 13 to 12 for better alignment.
- Updated the removeSketchMermaidShortcutHints function to also remove the submit shortcut hints from the dialog, enhancing the user interface by decluttering unnecessary elements.
- Adjusted tests to verify the absence of shortcut hints in the modal after updates.

* fix(web): address plan mode follow-up polish

* fix(web): align ci expectations after merge

* test(e2e): stabilize project workspace helpers

* test(e2e): scale settings visual timeout

* fix(prompts): lock ElevenLabs voice picker choices

* test(e2e): pin project workspace P0 worker

* test(e2e): use rail new project entry

* test(e2e): relax app restoration startup waits

* fix(web): limit markdown pipe escaping to tables

---------

Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
Co-authored-by: Amy <1184569493@qq.com>
2026-07-01 04:38:31 +00:00
54de349c47 fix: replace expired Discord invite (#4849)
Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local>
2026-06-29 15:00:45 +00:00
f80ad9e2dc fix(plugins): add bundled Perplexity AI design-system plugin manifest (#4828) (#4833)
The Perplexity AI system ships a card on /plugins/systems/ from
design-systems/perplexity/, but had no matching bundled-plugin manifest
under plugins/_official/design-systems/perplexity/. As a result
detailHrefForSystemSlug() found no plugin and the card fell back to the
/plugins/systems/ index instead of opening a dedicated detail page.

Add the bundled plugin for perplexity:
- open-design.json: generated via the migrate-to-plugins design-system
  wrapper, then brought to parity with sibling systems (specVersion,
  full title_i18n/description_i18n, localized useCase query). Structure
  is byte-identical in shape to the other design-system manifests.
- DESIGN.md: embedded copy of design-systems/perplexity/DESIGN.md.
- open-design-marketplace.json: register the bundled plugin in the
  official registry seed (sorted insert + bundledPreinstallCount 413->414),
  matching how prior plugin-add commits regenerate the seed.

The landing page now builds a real /plugins/design-system-perplexity/
detail page and the systems-index card links to it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:55:29 +00:00
1319bcef2e chore: prune curated official example plugins (#4815)
Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
2026-06-26 10:01:44 +00:00
3a92f8a011 Add web effect extractor scenario (#4811)
Co-authored-by: tuolaji <tuola@tuolajideMacBook-Air.local>
2026-06-26 07:15:53 +00:00
Tom HuangGitHubCursorxne998808-aiPerishCodeopen-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>lefarcenSiri-Ray
29b138f7a3 feat(brands): turn any brand into a reusable design system (#4691)
* Implement brand management routes and CLI support

- Added `brand-routes.ts` to handle HTTP endpoints for brand operations: listing, extracting, retrieving details, deleting, and serving logos.
- Introduced `brands-cli-help.ts` for CLI commands related to brand management, including usage instructions for listing, creating, retrieving, and deleting brands.
- Updated `cli.ts` to integrate brand commands into the existing CLI structure, allowing headless management of brands via the command line.
- Created supporting files for brand metadata handling, including `design-md.ts` for rendering brand information in markdown format and `index.ts` for the brand engine API.
- Implemented `prefetch.ts` to fetch and process brand material from specified URLs, ensuring a streamlined extraction process.
- Enhanced server setup in `server.ts` to register brand routes and manage brand-related data effectively.

This commit establishes a comprehensive framework for managing brands within the application, facilitating both HTTP and CLI interactions.

* Enhance memory management and onboarding experience

- Introduced canonical profile labels to ensure consistent handling of user input in profile forms, preventing duplicate entries.
- Updated the `parseProfileBody` and `captureProfileFromForm` functions to utilize the new canonical label matching.
- Added a memory callout section in the onboarding view to highlight the benefits of memory usage, including personalized responses and reduced setup questions.
- Implemented new UI elements in the onboarding view to improve user engagement with memory features.
- Expanded i18n support for new onboarding messages related to memory benefits across multiple languages.

* Refactor onboarding flow and enhance design system integration

- Updated the onboarding process to include a new brand extraction step, replacing the previous newsletter step.
- Adjusted the tracking logic to reflect the new onboarding steps, ensuring accurate analytics for user progress.
- Improved the UI for the onboarding view, including new input fields for email collection during the brand extraction phase.
- Refined the EntryShell component to remove outdated comments and clarify the onboarding renderer's purpose.
- Enhanced CSS styles for the onboarding steps to improve layout and user experience.
- Updated internationalization strings across multiple languages to reflect changes in the onboarding flow and brand extraction messaging.

* Add brand management features and enhance font handling

- Introduced new modules for managing brand assets, including `chrome.ts` for headless Chrome operations and `fonts.ts` for self-hosting web fonts.
- Implemented `prefetch.ts` to streamline the brand material extraction process, allowing for efficient harvesting of colors, fonts, and logos.
- Enhanced the brand system with new schema definitions in `schema.ts` to support brand color and font management.
- Developed the `engine` module to integrate brand building and rendering processes, including token derivation and artifact generation.
- Improved the overall structure and organization of brand-related files for better maintainability and scalability.

* Enhance brand extraction and project management features

- Updated `brand-routes.ts` to include new dependencies for project management, allowing for the registration of brand-related projects.
- Modified the `extractBrand` function to support project ID and system files, improving the brand extraction process.
- Enhanced the CLI commands in `cli.ts` to handle project IDs during brand creation, enabling better tracking of brand projects.
- Updated the server setup in `server.ts` to register new project-related routes.
- Improved the UI components to display project information associated with brands, including buttons for opening projects in the `BrandDetailView` and `BrandsTab`.
- Added new metadata fields in the contracts to support project tracking and management for brands.

This commit establishes a more robust framework for managing brand projects, enhancing both backend and frontend functionalities.

* Enhance onboarding profile management and memory persistence

- Added new canonical profile labels for 'Organization size', 'Use cases', and 'Discovery source' to improve user input consistency.
- Introduced `OnboardingProfileState` type to manage onboarding profile data more effectively.
- Implemented functions to build and persist the onboarding profile body to memory, ensuring user selections are saved accurately.
- Updated the `OnboardingView` component to handle profile persistence during navigation and submission steps.
- Enhanced tests to verify that user selections are correctly persisted to the memory profile.

This commit improves the onboarding experience by ensuring that user inputs are consistently captured and stored, enhancing overall user engagement with the application.

* Reflow brand extraction into an agent-driven, live flow

Replace the deterministic SSE prefetch/preview/system pipeline with an
agent-driven extraction: POST /api/brands now reserves the brand and stands
up a backing project with the target site open in an in-app browser tab plus
a seeded prompt, so the agent measures, synthesizes brand.json incrementally,
and the user can clear anti-bot walls by hand. New /preview and /finalize
routes let the agent render the kit page live and register the resulting
user:<id> design system, so extracted brand facts persist as a structured,
reusable brand kit instead of a one-shot deterministic guess.

Adds the brand-extract skill (SKILL.md + brand-kit.html template), kit-render
engine, brand-extraction-engine tests, brand project covers in the Designs
tab, onboarding extract handoff, and the matching od brand extract/preview/
finalize CLI subcommands and contract updates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Sediment finalized brands into structured memory

Reflow a finalized brand into the memory store (brandToMemoryEntries +
reflowBrandToMemory) so future chats can ground vague requests in the
brand's palette, type, voice and rules. finalizeBrand now wires through
the runtime dataDir and best-effort persists the brand, MemoryChangeEvent
gains a 'brand' source, and the brand kit render hardens its inline JSON
escaping. Adds brand.previewEmpty / brand.viewDetails across all locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Implement logo fallback and imagery support in brand extraction

- Introduced a deterministic logo fallback mechanism to ensure that brand extraction processes can retrieve and save site logos, even when the agent fails to do so.
- Enhanced the `startBrandExtraction` and `finalizeBrand` functions to utilize the new logo fallback, allowing for better handling of logo assets.
- Added support for imagery samples in brand validation, enabling the inclusion of representative images in the brand kit.
- Updated the brand kit rendering to include self-hosted fonts and imagery, improving the overall presentation of brand assets.

This commit strengthens the brand extraction workflow by ensuring that logos and imagery are reliably captured and displayed, enhancing the user experience in brand management.

* Enhance memory management with rule proposal and verification features

- Introduced new functionality for distilling annotations into rule proposals, allowing users to suggest rules based on in-canvas annotations through the `od memory rule suggest` command.
- Implemented a verification system that programmatically enforces compliance with active rules during artifact generation, ensuring that all active rules are covered in the self-verify scorecard.
- Added endpoints for managing verification outcomes, including listing, removing, and clearing verification records, enhancing the transparency of the verification process.
- Updated the memory management system to support the retrieval of active rule entries, ensuring that only linked rules are considered during verification.
- Enhanced tests for both rule proposal generation and verification processes to ensure reliability and correctness.

This commit strengthens the memory management capabilities by integrating rule proposals and verification, improving the overall user experience in managing design rules and ensuring compliance.

* Distill review annotations into memory and enforce self-verify scorecard

Add distillAnnotationsToMemory to mine inline preview comments/highlights/
marks into durable feedback + rule memory via a dedicated distiller prompt,
threaded through the existing extract pipeline with an 'annotation' change
source. Tighten the self-verify prompt (daemon + contracts) to state the
daemon programmatically checks the scorecard, so a missing or uncovered
scorecard on an artifact turn is an enforcement failure. Cover the rule
suggest and verification-history routes with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply brand design system through web config on "Use in new chat"

Thread onApplyDesignSystem from the entry shell into BrandsTab so the brand's
registered design system is applied via the web config channel instead of a
bare daemon PATCH that left the Home composer stale. Add a transient
home-intent latch + event so the Brands tab can request the Prototype chip on
the already-mounted HomeView, which consumes it once the plugin catalog loads.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Wire annotation distillation into background memory extraction

Add a background distill pass that mines inline review annotations
(comments / highlights / drawn marks) from a turn into durable memory
alongside the general LLM extraction, surface an `annotation` memory
toast source in the web UI, and cover the flow with a unit test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix brand design system not applying to composer on "Use in new chat"

Selecting a brand's "Use in new chat" applies the brand's design system as
the default and fires the Prototype chip intent in the same synchronous click
handler. HomeView consumed that intent inside the event listener, so `pickChip`
ran before React committed the config change and seeded the composer's
design-system field from the stale (empty) default — the composer showed
"No design system" instead of the brand until a reload.

Split the intent handling: the listener now only bumps a tick, and a separate
effect consumes the chip after the re-render lands, so the seeded design system
reflects the freshly-applied brand. Add the previously-untracked home-intent
latch test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): rework Brands into Brand Kit and add Home create entry

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): harvest real cover/hero images for the Images module

The brand kit's Images gallery only populated when the extraction agent
remembered to save imagery — so a forgetful or bot-blocked agent (and the
pre-imagery "Open Design" brand) left it empty. Add a deterministic,
server-side imagery fallback (imagery-fallback.ts), mirroring the logo
fallback: it parses og:image/twitter:image, large <img> (highest-res
srcset/<picture>), <link rel=preload as=image>, and CSS background-image
hero blocks, fetches candidates with browser-shaped headers, decodes
PNG/GIF/JPEG/WebP dimensions to keep only big representative images
(dropping icons/sprites/logos/tracking pixels), dedupes by content hash,
and saves up to 8 of the largest into imagery/ with labeled samples.

finalizeBrand runs it as a timeout-bounded, failure-tolerant safety net
(injectable so tests stay offline) when the agent captured too few
samples, first adopting any on-disk images. The extraction prompt and
brand-extract SKILL now explicitly direct the agent to harvest the site's
large/cover/hero images, filtered by rendered size.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(qa): implement deck layout validation and safety checks

Add a new QA module for validating the layout of generated brand decks to ensure robustness against clipping and truncation issues. The `analyseDeckLayout` function checks for critical layout invariants, including the presence of `.slide` sections, correct container types, and necessary runtime layers. Introduce `assertDeckLayoutSafe` to enforce these checks during brand system rebuilds, preventing the deployment of decks that fail validation. Additionally, create comprehensive tests to verify the functionality of the new layout validation features.

* fix(brands): apply deck shrink-to-fit synchronously so slides never clip

The no-clip runtime scheduled its fit pass through requestAnimationFrame,
whose callbacks are throttled while the deck is offscreen or occluded. A
slide could therefore stay unscaled — and clip its content — until first
paint. Fit synchronously on resize/load/fonts-ready with a trailing
setTimeout settle pass for late reflow, removing the rAF dependency.

Verified at the previously-broken 1024x620 viewport: container-type:size,
zero truncations, runtime auto-applies scale (Problem 0.71, ASK 0.87,
Product 0.97, Competition 0.97) and frame clip count drops to 0.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): let New Brand modal embed scrollable brand reference picker

Add a fillHeight mode to BrandReferencePicker so the heading, quick-pick
row and controls stay pinned while only the gallery scrolls inside a
bounded-height parent. Wire it into NewBrandModal with a stable, spacious
dialog and refresh the related newBrand/brandPicker copy across all 18
locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): enhance brand extraction with deterministic seed harvesting

Introduce a new `seed-fallback` module to provide a server-side deterministic palette and typography seed during brand extraction. This ensures that the brand kit's initial display includes a harvested logo, an approximate color palette, and font families, improving the user experience by reducing the all-skeleton appearance during the first paint. Update the `startBrandExtraction` function to utilize this new module, allowing for a more seamless and visually appealing brand extraction process.

Additionally, enhance the `BrandReferencePicker` component to reflect loading states and errors during brand extraction, ensuring users receive immediate feedback on their actions. Update related tests to verify the idempotency of the `finalizeBrand` function, ensuring that re-finalizing a brand correctly reuses the existing design system without duplication.

* feat(brand-extract): enhance BrandReferencePicker and localization updates

Updated the BrandReferencePicker component to reflect loading states and errors during brand extraction, improving user feedback. Added a new localization key for the brand extraction process and updated existing translations in English, Simplified Chinese, and Traditional Chinese to enhance clarity and user experience. Additionally, introduced new styles for better interaction with brand assets in the brand kit template.

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(brands): wire in-page lightbox/masonry/asset preview + refine seed

Brand-kit preview improvements for the live extraction kit:
- brand-kit.html: add in-page overlay system (sandboxed iframe has no
  top-nav) — clickable image lightbox with prev/next, a "view all"
  masonry modal, and a full-page asset preview modal that loads
  system/artifacts/<kind>.html in an iframe. Defer auto-reload while an
  overlay is open so it never yanks the modal out mid-interaction.
- seed-fallback.ts: prefer vivid mid-luminance hues for the seeded
  accent/accent-secondary, and drop icon/symbol faces (Remix Icon etc.)
  from the typography seed so specimens never render glyph soup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* i18n(web): add brandPicker.opening across remaining locales + picker test

Completes the brand-reference picker i18n key that was committed only for
en/zh-CN/zh-TW, so every locale satisfies the typed Dict, and lands the
BrandReferencePicker extraction-feedback test left untracked by the
concurrent worker.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(EntryShell): enhance AMR cloud card visibility post-detection

Updated the EntryShell component to ensure the AMR cloud card remains visible after detection settles, even when the AMR runtime is unavailable. This change prevents the card from disappearing and allows it to degrade gracefully to fallback content and sign-in flow. Additionally, added tests to verify the new behavior, ensuring a better user experience during onboarding.

* feat(library): OD Library asset registry + OD Clipper extension

Add a global, cross-project asset registry (OD Library) and a Chrome MV3
capture extension (OD Clipper), wiring the full HTTP + CLI + Web UI three-track
loop per specs/od-clipper.md.

- contracts: LibraryAsset/Source/Kind, ingest, search, pairing, task DTOs
- daemon: 6 additive SQLite tables, content-addressed owned storage, the
  idempotent registerLibraryAsset hook (hash dedup + append-source),
  programmatic enrichment (mime/size/image dims/domain/tags), pairing tokens
  with a persisted extension-origin allowlist, /api/library/* routes, and
  /api/tools/library/{search,apply} for in-task agent reuse
- cli: `od library list|get|rm|search|import|pair`
- web: Library tab (grid, source badges, filters, search, live SSE updates,
  extension pairing affordance)
- clipper/: standalone MV3 extension (background SW, content toolbar, popup)
- skills/library-curator: utility skill for agent-driven asset reuse

Origin middleware now honors paired chrome-extension:// origins (seeded from
SQLite on boot) and exempts the pairing-confirm handshake. Enrichment AI stages
(caption/OCR/embedding) are recorded as skipped pending a configured model.

* feat(brands): programmatic-first design system extraction + rename

Make brand extraction two-phase so a usable design system is ready the
moment the user enters a URL — the instant "aha" — instead of waiting on
the AI agent:

1. PROGRAMMATIC-FIRST (synchronous): startBrandExtraction now harvests the
   site deterministically (logo, palette, typography, one-line description,
   cover imagery, source URL) via prefetchBrand, synthesizes a valid design
   system with brandFromMaterial (no LLM), and finalizes + registers it
   before returning. finalizeBrand is refactored into a reusable
   finalizeBrandCore shared by both the programmatic path and the agent path.
2. ASYNC AI ENRICHMENT: the seeded agent prompt is reframed to enrich the
   already-usable design system and re-finalize in place (same user:<id>),
   updating every artifact/template.

Bounded + best-effort: a blocked/unreachable origin skips phase 1 and stays
`extracting` for the agent to drive. Gated on userDesignSystemsRoot so the
legacy agent-only path stays intact for tests.

Also rename the user-facing "Brand Kit" surface to "Design System" across
en + zh-CN strings, project names, and the enrichment prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(library): enhance asset import and management features

- Updated the `import` command to allow multiple local files and remote URLs, with restrictions on supported formats.
- Added new commands: `apply` for copying assets into project design files, `edit-as-page` for converting HTML assets into editable projects, and `figma` for exporting Figma captures.
- Introduced sidecar functionality for storing derived data alongside owned assets, including Figma capture IR and element HTML.
- Enhanced server configuration to support larger ingest payloads for asset captures.
- Improved error handling and user feedback during asset import and application processes.

* feat(asset-management): enhance asset dropzone and introduce chat-to-design feature

- Updated the DesignSystemAssetDropzone component to improve file preview handling with new functions for creating and revoking object URLs.
- Adjusted CSS for better layout and spacing in the asset dropzone.
- Added a new "Chat to design" button in the LibrarySection component, allowing users to send selected assets to the Home chat composer for project creation.
- Updated localization strings across multiple languages to reflect changes in asset import terminology.
- Enhanced the HomeView component to handle asset staging from the chat composer.

* feat(library): enhance asset application with element markup support

- Updated the `applyLibraryAsset` function to include an `includeElement` option, allowing the capture of element markup alongside assets.
- Modified related components (e.g., `ChatComposer`, `LibrarySection`, `FileWorkspace`) to handle the new element markup feature, ensuring both asset paths and optional element paths are returned and processed.
- Introduced a new function, `fetchLibraryAssetElementHtml`, to retrieve the captured HTML for element-pick assets.
- Enhanced the UI to display element markup inline within the chat composer, improving user interaction with captured elements.
- Updated API contracts to reflect changes in asset application responses, including optional element markup paths.

* feat(library): enhance asset filtering and preview handling

- Updated the LibraryPicker and LibrarySection components to implement a badge-aware kind filter, allowing for more precise asset filtering based on badge kind.
- Introduced a new `matchesKindFilter` function to streamline the filtering logic across components.
- Enhanced the DesignSystemAssetDropzone to ensure proper handling of image previews, addressing issues with broken thumbnails under React StrictMode.
- Added CSS styles for kind badges to improve asset representation in the UI.
- Implemented tests for the DesignSystemAssetDropzone to ensure correct preview lifecycle management.

* feat(library): hydrate single asset on SSE ingest

Add fetchLibraryAsset(id) so the Library grid can merge just the one
asset an `ingest` SSE event references instead of refetching the whole
list on every capture. Returns null on miss/error.

* feat(clipper): richer in-page image picker

Collect CSS background-image url()s in addition to <img> (so hero/section
art painted as backgrounds is no longer silently missed), defer thumbnail
decode to visible cells via IntersectionObserver, draw downscaled canvas
thumbnails instead of second full-res decodes, and add locate-on-page
highlighting so a picked image can be traced back to its DOM source.

* feat(library): implement lazy loading for thumbnails and enhance asset filtering

- Introduced a `LibraryThumb` component to lazily load heavy content (images, videos, iframes) only when they are near the viewport, improving performance.
- Added a debounced search feature to optimize asset filtering, reducing unnecessary network requests during rapid input.
- Enhanced the asset filtering logic to track active filters using a ref, ensuring efficient updates during live events.
- Updated the `snapshotCardRects` and `cardIdsInBand` functions to support improved hit-testing for drag-and-drop interactions.

* feat(library): lazy picker thumbnails + debounced search

Extend the Library grid's lazy-thumbnail + 250ms debounced-search pattern
to the composer LibraryPicker so opening it no longer fires one full-bytes
request per asset, and tidy the clipper content-script image collection.

* feat(clipper): compress and budget capture inlining

Re-encode large raster images to downscaled WebP and inline smallest-first
within a fixed budget, dropping only the secondary Figma IR past a safe body
size, so an image-heavy page (e.g. a news front page) always saves as an
editable HTML capture instead of 413-failing the ingest.

* test(library): LibraryPicker debounce + lazy-thumbnail coverage

Cover the composer picker's 250ms debounced search and its lazy <img>
mount (deferred until the card is in view), matching the grid's perf test.

* feat(design-system): enhance asset handling and UI for design systems

- Updated the CLI to support additional asset kinds, including 'design-system'.
- Enhanced the DesignSystemProvenance type to include source URLs, improving provenance tracking.
- Modified the design system generation jobs to correctly summarize source links and GitHub repositories.
- Updated UI components to reflect changes in asset handling, including new source link management in the DesignSystemFlow.
- Improved tests to cover new functionality for adding source links and ensuring proper handling of design system assets.

* refactor(library): rename 'design-system' to 'brand kit' and enhance thumbnail loading

- Updated labels and filters in Library components to replace 'design-system' with 'brand kit'.
- Introduced a shimmer skeleton for lazy-loaded thumbnails in the LibraryPicker to improve user experience during asset loading.
- Enhanced the PickerCard component for better performance by memoizing individual asset cards.
- Updated tests to ensure proper handling of brand kit assets and their visibility in the LibraryPicker.

* feat(clipper): implement internationalization for toolbar and popup

- Added i18n support to the clipper, enabling localization of UI elements and tooltips.
- Introduced a new i18n.js file to manage translations for various languages.
- Updated content.js and popup.js to utilize the i18n functions for dynamic text rendering.
- Enhanced accessibility by ensuring aria-labels and tooltips are also localized.
- Improved user experience by providing localized messages for actions and statuses.

* feat(clipper): enhance brand kit extraction and localization support

- Updated the brand kit extraction process to include improved handling of assets and localization for various UI elements.
- Added internationalization support for the brand kit feature, allowing for dynamic text rendering based on user locale.
- Enhanced the user experience by ensuring that all relevant messages and tooltips are localized.
- Updated tests to cover new localization features and ensure proper functionality of the brand kit extraction process.

* feat(clipper): enhance brand color derivation and update localization

- Introduced new functions for color manipulation, including linear interpolation and clamping, to improve brand color derivation.
- Updated the deriveBrandColors function to better map observed palettes to semantic roles, ensuring consistent brand representation.
- Revised localization strings in i18n.js to reflect changes from 'brand kit' to 'design system', enhancing clarity and user experience.
- Improved overall code organization and readability by refactoring existing functions and adding new utility methods.

* refactor(clipper): update terminology from 'brand kit' to 'design system'

- Replaced all instances of 'brand kit' with 'design system' across various components and localization files for consistency.
- Updated UI elements, tooltips, and documentation to reflect the new terminology.
- Enhanced user experience by ensuring clarity in the design system extraction process and related functionalities.
- Adjusted localization strings in multiple languages to align with the updated terminology.

* feat(clipper): enhance image fill handling and normalization

- Introduced functions to normalize image fills by converting non-PNG/JPEG formats (SVG, WebP, GIF, AVIF) to PNG before import, ensuring all images are properly rendered in Figma.
- Updated the UI to report the number of images converted and dropped during the import process, improving user feedback.
- Enhanced the overall image processing workflow to prevent silent failures when unsupported formats are encountered.
- Revised documentation to reflect the new image handling capabilities and supported formats.

* feat(clipper): enhance UI kit and busy state feedback

- Updated the UI kit to include new components such as inputs, selection, and overlays, improving the overall design system representation.
- Enhanced the busy state feedback during capture processes with localized messages and a step-by-step progress indicator, providing users with clearer status updates.
- Revised localization strings to support new UI elements and improve user experience across multiple languages.
- Improved documentation to reflect changes in the UI kit and busy state handling.

* fix(brands): restore design-systems nav entry + reconcile BrandsTab on re-activation

Address review feedback on PR #4260:

1. EntryNavRail dropped the only control that reached view==='design-systems'
   when Brands replaced it in the rail, leaving the still-rendered/routed
   design-system manager deep-link only (the entry-nav-design-systems e2e
   specs assert this). Restore a reachable rail entry (blocks icon, existing
   navDesignSystems key) alongside Brands.

2. BrandsTab only fetched once on mount, but EntryShell keeps sub-views
   mounted and toggles visibility, so a brand finishing extraction in its
   backing project never reconciled until a full reload. Refresh whenever the
   Brands view becomes active again, and poll while any brand is extracting
   (torn down once settled / when hidden).

Red spec: tests/components/BrandsTab.refresh.test.tsx (fails pre-fix:
fetchBrands called once, not twice).

* Update clipper/brand-capture.js

* fix(clipper): improve busy state handling and UI feedback

- Adjusted the spinner CSS to use flex properties for better layout control.
- Enhanced the reclampIfMoved function to preserve user position during busy state transitions.
- Added loading toast notifications for popup-launched captures to ensure progress visibility even when the on-page bar is hidden.

* feat(daemon): add kiwi-schema dependency and enhance Figma API integration

- Added kiwi-schema package to the daemon for improved schema handling.
- Updated FigmaApiNode interface and related functions to support shared functionality with the offline decoder, ensuring consistency in node processing.
- Refactored capture functions in clipper to maintain UI visibility during DOM/IR snapshots, enhancing user experience during capture operations.

* fix(web): surface missing backing projects

* fix(web): re-enable brand actions after use

* fix(daemon): serve brand logos from data roots

* fix(brands): reconcile failed extractions

* feat(daemon): implement offline Figma import and decoding functionality

- Added support for importing `.fig` files directly into the daemon, enabling offline processing without requiring a Figma account.
- Introduced a new `fig-decode.ts` module for decoding `.fig` files, handling both ZIP-wrapped and raw formats.
- Created `figma-import.ts` to orchestrate the import process, generating a canonical snapshot and associated metadata.
- Enhanced the server to handle Figma file uploads and integrate with the new decoding logic.
- Updated package dependencies to include `kiwi-schema`, `html2canvas`, and `jspdf` for improved functionality.
- Added tests for the new Figma import features to ensure reliability and correctness.

* feat(clipper): reload-proof capture progress badge on the extension icon

The on-page progress strip dies if a page reloads itself mid-capture
(aggressive paywall sites like economist.com do this), leaving no
loading signal. Add a per-tab '•••' badge on the extension icon for the
lifetime of any capture message — it lives on the action icon, so a page
navigation can't take it down. Verified end-to-end via a real loaded
extension.

* feat(daemon): add export functionality for Figma and enhance PDF export process

- Introduced `runFigma` command for importing Figma designs, supporting both local `.fig` files and Figma URLs.
- Added detailed usage instructions for the `od figma import` command.
- Implemented `runExport` command for programmatic export of HTML/deck artifacts to PDF, PPTX, or image formats.
- Enhanced error handling and user feedback during export processes.
- Removed obsolete `build-pptx-export-prompt` module and related tests to streamline the codebase.

* feat(daemon): enhance library synchronization and export capabilities

- Implemented `reconcileLibrary` to mirror design systems and agent-produced project deliverables into the Library as referenced assets.
- Added support for programmatic export of artifacts via the `od export` command, including detailed usage instructions.
- Introduced new functions for handling Figma imports and exports, improving integration with design workflows.
- Enhanced error handling and user feedback during synchronization and export processes.
- Added tests for new features to ensure reliability and correctness.

* feat(web): PPTX export for any shareable artifact + Library toolbar tooltips

* chore(nix): refresh pnpm deps hash

* refactor(web): enhance onboarding view and file export progress indicators

- Updated the onboarding view layout for improved accessibility and visual hierarchy, including adjustments to spacing, typography, and button styles.
- Introduced a loading toast for file export progress, displaying elapsed time and estimated time remaining for slide exports.
- Added new translation keys for export progress messages in multiple languages.
- Refactored the export progress handling to provide real-time updates during the export process, improving user feedback and experience.

* refactor(web): streamline export capture bridge and update connector styles

- Removed unused loading logic for html2canvas in the export capture bridge, simplifying the code.
- Updated CSS for the onboarding view connector to improve visual clarity and ensure it does not overlap with node numbers.

* refactor(web): remove html2canvas dependency and enhance Figma URL handling

- Removed the html2canvas package from the project, including its references in the lock file and related components.
- Added functionality to manage Figma URLs within the Design System flow, allowing users to add, remove, and validate Figma file links.
- Improved drag-and-drop handling to prevent unintended file navigation when dropping files outside designated areas.
- Updated UI components to accommodate new Figma URL features, enhancing user experience and accessibility.

* refactor(web): unify brand and design system flows

- Merged the brand extraction process into the design system creation workflow, allowing users to start from a brand directly within the design system wizard.
- Updated routing to redirect legacy brand links to the unified design systems tab.
- Enhanced the onboarding experience by removing the separate Brand Kit tab and integrating brand selection into the design system creation process.
- Improved UI components to reflect these changes, ensuring a seamless user experience across the application.

* feat(web): introduce brand enrichment banner and picker modal

- Added a new BrandEnrichmentBanner component to allow users to refine programmatically-extracted design systems with AI by selecting design-system skills.
- Implemented a BrandPickerModal for selecting brands from a searchable gallery, enhancing the design system creation flow.
- Updated ChatPane to conditionally display the enrichment banner for eligible brand projects, improving user engagement.
- Enhanced the design system flow to support the new brand enrichment features, ensuring a seamless experience for users.

* feat(web): enhance BrandPickerModal and DesignSystemAssetDropzone

- Updated the BrandPickerModal to allow scrolling of the entire picker area, improving user experience by creating a unified scrolling surface.
- Added new props to the BrandReferencePicker for action labels and scroll root reference, enhancing flexibility in brand selection.
- Introduced a new DesignKitView component for rendering design kits consistently across different surfaces.
- Enhanced the DesignSystemAssetDropzone to support a wider variety of file types with appropriate previews, improving asset management during design system creation.
- Updated styles for better visual clarity and responsiveness across components.

* feat(web): update Design Systems tab actions and enhance localization

- Changed the button label in the DesignSystemsTab from "Edit" to "Open" for better clarity in user actions.
- Added a new translation key for 'dsManager.openSystem' across multiple languages to support the updated button label.
- Enhanced the FileWorkspace component to ensure the Design Files tab aligns correctly with the Design System tab, improving UI consistency.
- Implemented a new design system editing feature that allows users to fetch and save design system content from DESIGN.md, enhancing the design workflow.

* fix(merge): repair post-merge regressions after origin/main integration

Follow-up fixes on top of the origin/main merge (886f925cd) addressing
regressions the conflict resolution introduced. main's web suite is the
oracle (100% green); resolution principle was main's engine/backend +
HEAD's UI, unioned.

daemon:
- library-sync.ts: correct design-systems import to ./design-systems/index.js
  (design-systems became a directory module on main).
- tests/server-bootstrap-regression: add LIBRARY_DIR to the PathDeps fixture
  (main-added test x HEAD-added LIBRARY_DIR field).

web:
- WorkspaceTabsBar: union — restore main's onboarding Search-popover close
  behaviour + guards, keep HEAD's library/brands nav entries.
- HomeView: restore main's composer sending-state (await onSubmit, widen its
  return type to Promise<boolean>|boolean|void, pass submitting to HomeHero).
- MemorySection.test: take main's version to match main's two-loop memory
  component.
- i18n: restore dropped settings.onboardingRoleMarketing key across types.ts
  and all locales.
- App/BrandsTab/EntryNavRail/router/home-intent: union fixes restoring main
  features dropped during conflict resolution (needs_input handling, etc.).

Validation: pnpm guard + full pnpm typecheck (all 23 packages) green.
Known-red: EntryShell onboarding step 3 intentionally retains HEAD's "build"
step rather than main's brand-extract step; 8 EntryShell.onboarding /
App.onboarding-amr-e2e tests stay red pending that onboarding decision.

* fix(merge): keep HEAD's unified brand flow (revert main's separate Brands tab)

Follow-up to 688544ff7. Per the chosen product direction (brand creation
unified into the design-system create wizard, not a standalone Brands tab),
revert the brand-flow routing/nav that the post-merge repair had restored
from main:

- router.ts: keep HEAD's brand routing (brands folded into design-systems),
  drop main's standalone /brands and /brands/:id view routing.
- EntryNavRail.tsx: drop main's standalone "Brands" nav button.
- runtime/home-intent.ts: drop main's brand "Use in new chat" confirmation
  notice plumbing (tied to the separate Brands flow).

Kept from the repair commit (additive, non-conflicting): App.tsx
loadedActiveProject correctness, composer Sending… state, WorkspaceTabsBar
onboarding popover behaviour, two-loop memory test, restored i18n keys,
brand needs_input STATUS handling, server.ts plugin-route infrastructure.

* feat(library-ui): implement conditional rendering based on LIBRARY_UI_VISIBLE

- Updated router.ts to conditionally render the library view based on the LIBRARY_UI_VISIBLE flag.
- Modified ComposerPlusMenu.tsx, DesignFilesPanel.tsx, and DesignSystemAssetDropzone.tsx to show the "Select from library" button only when LIBRARY_UI_VISIBLE is true.
- Adjusted EntryNavRail.tsx and EntryShell.tsx to include the library navigation button and section conditionally based on the LIBRARY_UI_VISIBLE state.
- Enhanced HomeHero.tsx to allow starting a blank project directly, improving user experience by providing more options for project creation.

This commit introduces a feature toggle for the library UI, allowing for better control over its visibility during development and testing.

* feat(home-hero): implement edge auto-scroll for horizontal overflow

- Introduced `useEdgeAutoScroll` hook to manage auto-scrolling behavior for horizontally overflowing components in HomeHero.
- Updated `PluginPromptPresets` and `RailGroup` components to utilize the new auto-scroll functionality, enhancing accessibility for users without trackpads.
- Added `EdgeScrollZones` component to provide interactive edge zones for scrolling.
- Enhanced CSS styles to support the new scrolling layout and ensure proper positioning of elements.

This commit improves user experience by making overflow content more accessible and easier to navigate.

* feat(design-systems): add project creation from design system and enhance UI components

- Implemented `handleCreateProjectFromDesignSystem` function in `AppInner` to facilitate project creation directly from a selected design system.
- Updated `DesignKitView` to wrap the iframe in a span for better layout control.
- Refactored CSS for `BrandPreviewCard` and `DesignSystemsTab` to improve styling and responsiveness.
- Introduced a new `TemplatePicker` component in `HomeHero` for selecting project-type templates, enhancing user experience.
- Updated various components to support asynchronous handlers for design system actions, improving overall functionality.

This commit enhances the design system integration and user interface, making project creation more intuitive and accessible.

* feat(brand-routes): enhance brand reservation API and add DESIGN.md support

- Updated the POST /api/brands endpoint to accept optional fields: description and designMd, allowing for more flexible brand reservations.
- Modified validation to require either a URL or designMd for brand extraction.
- Introduced a new design-md-input module to handle parsing and validation of DESIGN.md content.
- Enhanced startBrandExtraction function to support processing of DESIGN.md, improving integration with design systems.
- Added utility functions for managing DESIGN.md input and output, streamlining the brand creation process.

This commit improves the brand extraction workflow by integrating DESIGN.md support, making it easier for users to create and manage brands.

* feat(chat-pane, design-kit-view): enhance chat functionality and design preview features

- Added `handleNextStepPromptAction` to `ChatPane` for setting draft prompts, improving user interaction.
- Introduced `nextStepVariant` to differentiate design system projects in `ChatPane`.
- Updated `DesignKitView` to include a button for previewing design kit covers, enhancing user experience.
- Implemented a modal for displaying design kit previews, allowing users to view content in a dedicated space.

These changes improve the chat interface and design kit interactions, making the application more intuitive and user-friendly.

* feat(brand-extraction): enhance DESIGN.md support and testing

- Added a new test case to validate brand extraction from DESIGN.md input without requiring a website.
- Implemented functionality to register brands directly from DESIGN.md, improving the brand creation workflow.
- Updated the `ChatPane` and `NextStepActions` components to handle design system-specific actions for projects, enhancing user experience.
- Enhanced localization files with new carousel hints and project brief options across multiple languages.

These changes streamline the brand extraction process and improve the overall functionality of the design system integration.

* feat(wireframe-examples): add annotated and greybox wireframe examples

- Introduced new wireframe examples for annotated and greybox styles, enhancing design system capabilities.
- Added HTML and JSON files for both wireframe types, providing templates for low-fidelity design mockups.
- Implemented SKILL.md documentation for each wireframe example, detailing usage and design specifications.

These additions improve the design toolkit, offering users more options for creating wireframes in various styles.

* feat(brand-extraction): refine Chrome fallback and enhance error handling

- Updated the Chrome fallback logic in the prefetch pipeline to clarify its purpose and usage as a diagnostic tool.
- Introduced environment variable checks to enable or disable system Chrome usage, improving control over the extraction process.
- Enhanced error messages in the DesignSystemCreationFlow component to provide clearer guidance on required inputs for creating a design system.
- Added regression tests to ensure that prompts do not instruct the agent to invoke a non-existent `brand-extract` skill, preventing potential failures during brand extraction.

These changes improve the robustness of the brand extraction process and enhance user experience by providing clearer instructions and error handling.

* feat(brand-extraction): enhance DESIGN.md input handling and introduce brand ready prompt

- Updated the BrandFromDesignMdInput interface to explicitly define the description property as optional with undefined.
- Enhanced the brand extraction prompts to clarify the inline brand-extract workflow, preventing confusion during the extraction process.
- Added a new BrandReadyPrompt component to notify users when a design system is ready for preview, improving user experience.
- Introduced CSS styles for the BrandReadyPrompt to ensure a visually appealing and user-friendly interface.
- Updated localization files to support new strings related to the brand ready prompt across multiple languages.

These changes improve the clarity and usability of the brand extraction process, providing users with timely feedback and a more intuitive interface.

* feat(brand-extraction): improve design system focus handling and localization updates

- Refactored the handling of browser tabs in the brand extraction tests to ensure proper validation of tab states.
- Enhanced the AppInner component to refresh design systems alongside templates, ensuring users see the latest updates without page reloads.
- Introduced a pending focus state in the DesignSystemsTab to manage design system selection more effectively after brand extraction.
- Added a BrandReadyPrompt in the ProjectView to notify users when a design system is ready for preview, improving user engagement.
- Updated localization files for Chinese (Simplified and Traditional) to reflect changes in terminology related to design systems.

These changes enhance the user experience by providing timely feedback and ensuring that the design system selection process is seamless and intuitive.

* fix(styles): adjust letter-spacing and enhance plus-menu trigger styles

- Set letter-spacing to 0 in design-system-flow.css for improved text clarity.
- Added styles for plus-menu trigger in plus-menu.css, including background, border, and hover effects to enhance user interaction and visual consistency.

These changes refine the design aesthetics and improve the usability of the plus-menu component.

* feat(tests): add design-system focus handoff tests

- Introduced a new test suite for validating the design-system focus handoff functionality.
- Implemented tests to ensure that the focus ID is correctly set, read, and cleared from session storage, preventing user selection hijacking.
- Added checks for scenarios where no focus ID is pending, enhancing test coverage for the design system's behavior.

These tests ensure the reliability of the design-system focus handling, contributing to a more robust user experience.

* feat(export): restrict image format options to PNG and JPEG

- Updated the image format options in the export functionality to only allow PNG and JPEG, removing WebP to prevent silent downgrades.
- Enhanced error handling to provide clear feedback when an unsupported image format is specified.
- Adjusted related documentation and comments to reflect the changes in supported formats across the application.

These changes ensure consistency in image export behavior and improve user experience by providing immediate validation errors for unsupported formats.

* feat(origin-validation): implement zero-config OD Clipper bypass for library requests

- Added a new function `isZeroConfigClipperLibraryRequest` to validate requests from locally-installed browser extensions targeting the `/library/` path.
- Updated the origin validation middleware to utilize this function, allowing unpaired browser extensions to access the `/api/library/ingest` endpoint while blocking other cross-origin requests.
- Enhanced tests to cover the new bypass functionality, ensuring correct behavior for both valid and invalid origins.

These changes improve the integration of browser extensions with the local daemon, enhancing user experience while maintaining security.

* feat(design-systems): add download functionality for design systems

- Implemented a new command `od design-systems download <id>` to allow users to download design systems as a .zip file, including all system files and a generated SKILLS.md usage guide.
- Updated the CLI help documentation to include usage instructions for the new download command.
- Enhanced the design systems API to support the download feature, ensuring only user design systems are accessible while handling errors for non-existent presets.
- Added localization strings for the new download functionality across multiple languages.

These changes enhance the usability of design systems by providing a straightforward method for users to obtain and share their design assets.

* feat(design-systems): enhance design system management and localization

- Introduced new UI components and styles for managing design systems, including buttons for downloading, refreshing, and resetting edits.
- Updated the DesignKitView to support direct actions for DESIGN.md editing, improving user interaction with design systems.
- Enhanced the DesignSystemDetail component to include download functionality and improved state management for design system edits.
- Added localization strings for new features, ensuring consistent user experience across multiple languages.
- Improved error handling and user feedback for design system operations, including download failures.

These changes streamline the design system management process, making it more intuitive and user-friendly while ensuring robust localization support.

* feat(tests): add comprehensive tests for design system archive functionality

- Introduced a new test suite for validating the `buildUserDesignSystemArchive` and `buildDesignSystemSkillsMarkdown` functions.
- Implemented tests to ensure correct packing of design system files, including the generation of a `SKILLS.md` guide and exclusion of internal metadata.
- Added checks for handling non-user IDs and scenarios where a design system already includes its own `SKILLS.md`.
- Enhanced the overall test coverage for design system functionalities, ensuring reliability and correctness in the design system archive process.

These changes improve the robustness of the design system features by ensuring thorough testing of critical functionalities.

* feat(figma-import): enhance CLI output and add Figma import endpoint

- Updated the CLI to conditionally log detailed import information based on the `--json` flag, improving usability for users who prefer JSON output.
- Introduced a new API endpoint for importing Figma files, handling file uploads and validating project existence, with appropriate error responses for missing files or invalid URLs.
- Added a dedicated route for the Figma import functionality, ensuring seamless integration with existing project workflows.

These changes improve the Figma import experience by providing clearer output options and robust error handling, enhancing overall user interaction with the CLI and API.

* feat(design-files): enhance DesignFilesPanel with new actions and styles

- Added new action buttons for opening a browser and creating a design system in the DesignFilesPanel, improving user interaction in the empty state.
- Updated styles for action buttons to enhance visual distinction and usability.
- Enhanced tests to verify the functionality of new actions in the DesignFilesPanel, ensuring they trigger correctly.

These changes improve the user experience by providing additional functionality and clearer visual cues in the design files interface.

* fix(ci): restore new project modal flow

* fix(ci): align design kit and onboarding checks

* fix(ci): sync bake preview workflow action

* fix(ci): include plugin preview helper scripts

* fix(review): harden brand source and preview flows

* fix(ci): stabilize web workspace tests

* fix(review): address latest blocking feedback

* chore(ci): retrigger validation after label update

* chore: re-trigger CI on updated main — needs-validation gate moved to merge_group (#4714)

* refactor(lightbox): implement portal for overlays to resolve z-index issues

- Updated the lightbox component to use React's createPortal for rendering overlays directly to the <body>, ensuring proper z-index stacking.
- Removed session mode toggle from HomeHero and adjusted related styles and tests accordingly.
- Cleaned up CSS by removing unused styles related to session mode toggle.
- Updated tests to reflect changes in the HomeHero component and its interaction with the design router.

* style(home-hero): remove focus halo from template search input

- Updated CSS to eliminate the global input focus outline and box-shadow for the template search field in the HomeHero component.
- Added a test to verify that the template picker search field maintains a clean appearance when focused.

* feat(design-system): add create design CTA and enhance design kit functionality

- Introduced a new `DesignSystemCreateCta` component to facilitate creating new designs from an active design system, enhancing user experience in the chat interface.
- Updated `ChatPane` to include the new CTA, allowing users to create designs directly from the chat.
- Enhanced `DesignKitView` with sticky header functionality for better accessibility while scrolling.
- Added new CSS styles for the `DesignSystemCreateCta` component to ensure a visually appealing and consistent design.
- Updated internationalization files to include new strings for the design system creation feature.

* feat(upload): enhance file upload handling and error recovery

- Introduced `sanitizePath` to preserve directory structures during file uploads, preventing issues with subdirectory paths.
- Updated `DesignKitView` and related components to utilize the new `sanitizePath` function for improved file name resolution.
- Added `KitErrorBoundary` component to gracefully handle rendering errors in the design kit, providing a user-friendly fallback.
- Implemented internationalization updates for new error messages and action confirmations related to uploads and error handling.
- Enhanced CSS styles for better visual feedback during error states and improved user experience.

* feat(design-kit): add keyboard shortcuts hint and enhance key handling

- Introduced a new keyboard shortcuts hint in the DesignKitView, providing users with quick access to essential actions (E edit, C copy, U upload, R refresh, ⌫ delete logo).
- Implemented a keydown event handler to manage keyboard shortcuts contextually within the design kit, improving user interaction and accessibility.
- Updated CSS for the shortcuts hint to ensure it remains low-contrast until hovered, enhancing the UI experience.
- Added internationalization support for new shortcut labels and hints across multiple languages.
- Adjusted DesignSystemsTab to prefer user logos for their systems, improving visual consistency.

* feat(design-system): introduce DesignSystemExtractionPanel and enhance design system interactions

- Added the `DesignSystemExtractionPanel` component to facilitate user interactions during design system extraction, providing a synthesized conversation view and next steps.
- Updated `ChatPane` to render the new extraction panel when a design system is active, enhancing user guidance.
- Introduced a new utility function `designSystemExtractionSource` to derive human-readable labels for design system sources.
- Enhanced internationalization support with new strings for extraction-related actions and prompts across multiple languages.
- Updated various components and tests to reflect changes in terminology and functionality, improving overall user experience.

* feat(project): add project deletion functionality and enhance design system interactions

- Introduced `onDeleteProject` prop in `ProjectView` to handle project deletion, improving project management capabilities.
- Updated `AppInner` to include the new delete project handler, enhancing user experience in project interactions.
- Enhanced `DesignKitView` and `DesignSystemsTab` with loading states and improved visual feedback during design system resolution.
- Removed deprecated `DesignSystemCreateCta` component and associated styles, streamlining the codebase.
- Updated internationalization files to reflect changes in project management terminology and actions.

* feat(design-kit): enhance internationalization and user feedback in DesignKitView

- Updated various labels and error messages in the DesignKitView to utilize internationalization functions, improving accessibility and user experience.
- Enhanced color input validation messages and added confirmation prompts for design system deletions in DesignSystemsTab and FileWorkspace.
- Introduced new props for handling design system project deletions, streamlining project management.
- Updated internationalization files to reflect new strings and translations for improved user guidance across multiple languages.

* refactor(design-kit): remove keyboard shortcuts hint and streamline header menu

- Eliminated the keyboard shortcuts hint from the DesignKitView, simplifying the header menu.
- Updated the sticky-header overflow menu to exclude upload, full-system preview, and shortcut help actions, focusing on essential project operations.
- Adjusted related tests to reflect the removal of the shortcuts hint and ensure accurate menu item visibility.

* feat(brand-routes): add extract-from-html endpoint for brand extraction

- Introduced a new POST endpoint `/api/brands/:id/extract-from-html` to re-run brand extraction using HTML rendered from the in-app browser after clearing anti-bot walls.
- Implemented error handling for missing HTML and brand not found scenarios.
- Enhanced the `extractBrandFromHtml` function to process the provided HTML and optional CSS, integrating it into the existing brand extraction workflow.
- Updated `prefetch` functionality to support extraction from pre-rendered HTML, improving the overall brand data retrieval process.

* chore(nix): refresh pnpm deps hash

* feat(brand-cli): add extract-from-html command for brand extraction

- Introduced a new CLI command `od brand extract-from-html` to facilitate brand extraction from pre-captured rendered HTML, allowing users to bypass anti-bot walls.
- Enhanced the command to accept optional CSS and base URL parameters, improving flexibility in extraction scenarios.
- Implemented error handling for missing HTML input and invalid brand IDs, ensuring robust user feedback.
- Updated the `BRAND_USAGE` documentation to reflect the new command and its usage details.
- Adjusted server configuration to accommodate larger payloads for the new extraction endpoint.

* feat(design-system): enhance design system extraction and browser tools

- Added a new script to collect CSS styles from rendered pages, improving brand extraction capabilities by capturing computed styles from cross-origin stylesheets.
- Removed the `DesignSystemExtractionPanel` component and its associated styles, streamlining the codebase.
- Updated `ProjectView` and `FileWorkspace` components to enhance design system interactions and improve user experience.
- Introduced new internationalization strings for design system phases and actions, ensuring better user guidance across multiple languages.

* feat(brand-assist): implement browser assist for brand extraction

- Added support for a client-side confirmation mechanism for the brand-browser-assist od-card, allowing users to extract brand information from the unblocked browser DOM.
- Enhanced the `ProjectView`, `ChatPane`, and `AssistantMessage` components to handle the new assist functionality, improving user interaction during brand extraction.
- Introduced new internationalization strings for browser assist prompts and messages, ensuring clarity and guidance across multiple languages.
- Updated the `useBrandReadyPrompt` hook to manage the state of the browser assist, providing a seamless user experience when dealing with anti-bot walls.

* feat(brand-prompt): enhance BrandReadyPrompt with refinement options

- Updated the BrandReadyPrompt component to include options for AI optimization and manual editing, allowing users to refine extracted brand systems.
- Added a new refinement nudge to inform users that automatic extraction may miss details, improving user guidance.
- Adjusted styles for the prompt and dismiss button for better alignment and visual consistency.
- Introduced new internationalization strings for the refinement features, ensuring clarity across multiple languages.
- Removed deprecated PPTX export functionality from the FileViewer component, streamlining the export options.

* refactor(export): remove PPTX export functionality and streamline export options

- Eliminated PPTX export support across various components, including CLI, desktop, and web, to simplify export formats.
- Updated documentation and help messages to reflect the removal of PPTX, ensuring clarity for users.
- Adjusted export-related types and constants to focus on PDF and image formats only, enhancing code maintainability.
- Improved user experience by refining export options and related UI elements.

* refactor(export): remove PPTX references and update export functionality

- Removed all instances of PPTX export functionality from the codebase, including related dependencies and comments.
- Updated export options to focus solely on PDF and image formats, enhancing clarity and maintainability.
- Adjusted UI components and tests to reflect the removal of PPTX, ensuring a streamlined user experience.
- Improved internationalization strings and documentation to align with the new export capabilities.

* chore(nix): refresh pnpm deps hash

* fix(onboarding): preserve selected runtime

* fix(brand): localize generated kit copy

* fix(onboarding): align first-run flow with main

* fix(nav): use palette icon for design systems

* fix(analytics): use design system onboarding step

* fix(ui): remove design system guide toggle

* fix(ui): position design system ready prompt

* fix(ui): space plugin task notice

* fix(web): restore home ask mode and design kit preview

* test(e2e): align onboarding visual capture

* test(e2e): align amr onboarding checks

* fix(brand): remove blocked reference brands

* feat(onboarding): show profile choices as chips

* fix(home): prefer design system cover art in recents

* test(e2e): select onboarding profile chips

* feat(brand-extraction): implement programmatic extraction transcript and UI enhancements for design systems

* feat(brand-extraction): enhance programmatic extraction with transcript agent support and UI improvements

* feat(brand-extraction): add transcript agent resolution and improve message handling in brand extraction

* fix(design-systems): stabilize loading state coverage

* test(e2e): align design system detail visual

* fix(brand-extraction): backfill programmatic transcripts

* fix(web): refresh ready brand design systems

* fix(brands): stabilize extraction handoff and seed colors

* fix(brands): return extraction transcript immediately

* fix(web): open new project modal from entry rail

* fix(editing): expose content edits for plain targets

* feat(file-viewer): implement manual edit draft dirty state tracking and reset logic

* feat(design-system): enhance project creation flow with conversation ID handling

* feat(brands): implement light theme handling for color extraction and seed generation

* feat(brands): add finalizeBrandProject function for brand project completion

* feat(file-workspace): add designSystemBrandId prop and update DesignSystemProjectPanel to use it

* Fix manual editing for brand kits

* fix(design-system): wait for project refreshes

* fix(web): open new project modal from rail

* fix(web): restore home ask mode toggle

* fix(web): sync brand color edits with seeds

* fix(web): stabilize design system workspace tests

* test(tools-pack): relax Windows resource cache timeout

* chore(pr): retrigger review after validation

* fix(web): surface design kit action progress

* fix(web): clarify brand next-step actions

* fix(web): cancel programmatic brand extraction

* fix(web): add design systems tab action feedback

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: xne998808-ai <xne998808@gmail.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-25 03:56:14 +00:00
0bf1b6d6b8 [codex] converge release workflows and stable dry-runs (#4390)
* fix(tools-pack): use junctions for Windows standalone peer deps

* fix(desktop): expose IPC during startup

* fix(tools-pack): preserve Windows inspect diagnostics

* fix(tools-pack): report Windows inspect status errors

* fix(packaged): use Electron net fetch for app protocol

* fix(packaged): load Windows renderer from web sidecar

* fix(desktop): show Windows packaged window during startup

* fix(packaged): disable Windows GPU startup

* fix(tools-pack): keep Windows core smoke observable

* fix(packaged): remove Windows startup probes

* fix(tools-pack): trace Windows desktop IPC status

* fix(tools-pack): add Windows IPC diagnose loop

* fix(release): default beta-s Windows updater feed

* chore: clean merged test eof

* refactor(release): unify prerelease channel model

* chore(release): close prerelease doc escape hatches

* refactor(release): converge release channel workflows

* fix(release): install toolchain in metadata jobs

* fix(release): build release package before contracts

* chore(release): bump development version to 0.10.1

* fix(e2e): seed windows packaged smoke runtime config

* fix(release): install toolchain for metadata publish

* fix(release): materialize betas metadata checkout

* chore(release): bump development version to 0.10.2

* fix(release): allow betas metadata cold start from s3

* fix(e2e): support betas packaged update scenarios

* fix(release): pass betas channel into packaged smoke

* fix(release): set betas channel during self-hosted builds

* fix(release): verify counted channel reservations

* fix(release): use pnpm cmd for betas windows publish

* fix(release): add betas manifest artifact fallback

* fix(release): skip beta-s public metadata fetch

* fix(release): read beta-s manifests from storage

* fix(release): cache beta windows tools-pack builds

* fix(release): inline beta mac tools-pack builds

* fix(pack): deep sign unsigned mac bundles

* docs(pack): document payload-first beta updater validation

* fix(release): align preview tools-pack cache flow

* fix(release): align prerelease tools-pack cache flow

* fix(release): pass github token to prerelease metadata

* fix(release): setup pnpm before feishu notify

* fix(release): add stable dry-run prepublish flow

* fix(release): accept completed prerelease metadata gate

* fix(release): require stable release branches

* fix(release): converge r2 access checks

* fix(updater): use release channel parser for defaults

* fix(updater): harden windows payload relaunch

* fix(release): converge updater smoke fixture contract

* test(e2e): require silent updater fixture output

* fix(release): align stable windows smoke build path

* fix(ci): include release workspace in validation

* fix(ci): repair release validation lanes

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

* fix(ci): restore zero-install Feishu notification

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-06-23 06:13:21 +00:00
b02d20e6b9 fix: unify Discord invite links (#4452)
Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local>
2026-06-17 08:25:16 +00:00
915548b4bd [codex] Fix social template English metadata (#4427)
* Fix social media template English metadata

* Remove social template description hard wraps

Generated-By: looper 0.9.10+codex.autoclean (runner=fixer, agent=codex)

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: Looper <looper@noreply.github.com>
2026-06-17 11:00:06 +08:00
lefarcenandGitHub f6cceb303d fix(plugins): unwrap cosmetic hard wraps in bundled manifest descriptions (#4406)
Picking a Home example-prompt card (or "Replicate this content" in the
plugin detail modal) seeds the composer with the plugin manifest's
`description`. Many descriptions were authored with cosmetic hard wraps
(a single newline mid-paragraph, ~75 cols, as if formatted for a
terminal/markdown source). The composer renders the seed with
`white-space: pre-wrap`, so those newlines survived as short ragged
lines that never filled the editor width — leaving a large blank gutter
on the right of the input box (e.g. the Trading Analysis Dashboard
template: 4 hard breaks, ~200px / 29% of the editor blank).

Reflow the offending `description` / `description_i18n` strings in the
56 affected bundled manifests into flowing text (wrapped lines joined,
real blank-line paragraph breaks preserved; CJK joins carry no space).
Surgical string edits only — manifest formatting/key order untouched
(146 lines changed across 56 files). The full build spec still reaches
the agent as plugin context (SKILL.md + example.html); only the
human-facing seed copy changed.

Add an e2e guard (plugin-description-hardwrap.test.ts) that fails if any
bundled manifest description carries an intra-paragraph hard wrap, so
the cleanup can't silently regress when new examples are added.
2026-06-16 22:39:55 +08:00
eb749841b4 fix(home): surface community slide decks on Home with letterbox-free 16:9 previews (#4350)
* fix(plugins): surface slides decks on Home instead of marketplace sources

The 23 community slides/deck plugins from #4127 were registered as a
restricted community marketplace source, so they showed up as installable
"available sources" cards (受限 + Install) instead of on Home. Relocate them
to plugins/_official/examples so they ship bundled — appearing in the Home
Community gallery and the deck example-prompt rail, exactly like the existing
zhangzara / swiss / guizang decks — and drop their community-registry entries.

- Move 23 plugins/community/<slug> → plugins/_official/examples/<slug>
  (upstream LICENSE / author / homepage / "Based on" attribution preserved)
- Rename manifest name community-<slug> → example-<slug> (bundled convention)
- Remove the 23 entries from plugins/registry/community/open-design-marketplace.json
  (the 3 pre-existing community entries stay)
- HomeHero: uncap the deck example-prompt rail (other chips keep the curated 18)
  so every bundled slide template is reachable straight from "All"
- curatedPriority: pin the 23 slides to the front of both the deck example rail
  and the Community Slides shelf

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(home): drop provenance tail from deck seeds & fix deck preview letterbox

Two polish fixes for the slide/deck plugins now surfaced on Home:

1. Example-prompt seed: strip a trailing source-attribution sentence
   ("…。移植自 foo/bar 的 baz 模板。" / "Based on …") from the composer seed so
   the prompt doesn't end on provenance boilerplate. Conservative — only fires
   when the final sentence opens with an attribution marker and real text
   precedes it; attribution woven mid-description (followed by a use-case
   sentence) and the gallery/detail descriptions are left intact. Latin '.'
   only ends a sentence at whitespace/EOL so "STYLE_PRESETS.md" / "github.com/…"
   don't mis-split.

2. Deck gallery preview: decks ship a fixed 16:9 stage that scales to its
   viewport, so the tall scroll-preview frame letterboxed it and showed a dark
   --stage-bg band above/below the slide. Tag deck cards with data-od-mode and
   give them a 16:9 frame the iframe fills natively (no transform, no pan), so
   the stage sits edge-to-edge with no band.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(home): deck preview frame to 16:9 so no white strip below the slide

The deck preview override targeted the inner html-frame while the gallery frame
kept its fixed 300px height, leaving a white strip under the 16:9 slide. Make
the gallery frame itself 16:9 for deck cards so the stage fills it exactly —
title bar + slide, nothing left over.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(decks): hide the page-counter chrome on all 23 community slide decks

The five upstream deck families each render a different page-counter ("1 / 10")
— a center pill (.deck-controls), an id="counter" badge, a fixed .deck-counter,
or a .count span inside a Shadow-DOM rail — so the home gallery thumbnails
showed it inconsistently. Per maintainer request, drop it everywhere rather
than unify: inject a display:none rule for the light-DOM counters and inline-
hide the Shadow-DOM .count span. Keyboard/touch navigation is untouched; only
the visual counter is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(home): fix deck letterbox band in the example-prompt rail tiles too

The earlier 16:9 deck-preview fix only covered the Community gallery cards; the
hero example-prompt rail tiles render the deck in a 4:3 (1440×1080) preview and
still showed a dark --stage-bg band on top. Tag deck preset tiles with
data-od-mode and give them a 16:9 preview the iframe fills natively, matching
the gallery treatment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(home): remove the ↗ open-external button from community gallery cards

The ↗ only rendered when a tile's preview was an HTML source, so it
appeared on some cards (most decks, a few prototypes) and not others
(images, srcless tiles) — inconsistent chrome across the shelf. Drop it
and the whole onOpenExternal pass-through (PluginCard → PluginsHomeSection
→ HomeView) plus the now-dead .plugins-home__gallery-open styles.

* fix(home): make the community search clear (×) button visible

The clear button uses the shared <Button> component, whose CSS-module
class set padding 6/12 and crushed the 12px × glyph to zero width inside
the 26px button (a blank grey square). The .plugins-home__search-clear
reset only out-specified the global `button {}` element rule, not the
component class — scope it under .plugins-home__search so the padding:0 /
20x20 sizing actually wins.

* fix(home): align deck example-prompt tile title row with other tiles

Deck preset tiles used a 16:9 `auto` preview row, so their card was
shorter than the fixed-150px sibling tiles; the flex rail stretched them
back up and the slack leaked into the title row, making deck titles ~6px
taller. Keep deck tiles on the standard 150px preview cell and center the
native 16:9 stage inside it (neutral margin, no dark band), so card and
title-row heights match prototype/HyperFrames/image tiles.

* fix(deck-bridge): mirror the visible class so reveal animations fire on navigation

Reveal-animation decks (the frontend-slides family) gate staggered
entrances on a separate `.slide.visible` class that the deck's own show()
adds alongside `.active`. The host bridge's setActive() flipped only the
active class, so bridge-driven page turns showed slide chrome but left
every .reveal child at opacity:0 — the body rendered blank. Mirror
`.visible` in lock-step with the active slide (only for decks that use it,
a no-op elsewhere). Adds a red→green regression test.

* fix(home): unpin the bare Frontend Slides template from the slides shelf

example-frontend-slides is the family-root template with a generic cover;
pinned first it read as filler leading the Slides shelf and deck chip.
Drop it from PINNED_SLIDE_PLUGIN_IDS so it falls to the uncurated tail
while the styled variants (Creative Voltage, Electric Studio, …) keep the
lead.

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:32:15 +00:00
c7e422d7a2 feat(plugins): unify all 56 official deck seeds with the Open Design product narrative (#4357)
* feat(plugins): unify all 56 official deck seeds with the Open Design product narrative

Apply the same canonical 10-section story used for the community slides
batch (sourced from the homepage structure doc + README) to every official
deck plugin seed that ships an example.html: cover, why, three claims,
numbers board (60K+ stars / 300+ contributors / 217+ plugins / 129 design
systems / 21 agents), 5-step workflow, multimodal outputs, capabilities,
agent ecosystem, testimonial, and CTA with GitHub/Discord/X links.

Scenario decks keep their framing with Open Design as the subject:
weekly-report/weekly-update become OD growth reports, course-module a
getting-started tutorial, testing-safety-alert the local-data-security
story, xhs-* decks OD-themed xiaohongshu notes, ib-pitch-book an investor
narrative, presenter-mode-reveal keeps its presenter-mode demo.
open-design-landing-deck only gets its numbers aligned to the canonical
figures. Skipped (no example.html seed): guizang-ppt, html-ppt, replit-deck.

Content-only edits: CSS tokens, layouts, decorations, animations, keyboard
nav, hash routing and functional JS untouched. zh 13 decks / en 43 decks
per existing typography. All 56 verified: script syntax, cover screenshot,
no fictional brand residue.

* fix(decks): wire dead Download/closing CTAs to real destinations

Review follow-up for #4145: primary Download CTAs now point to
https://open-design.ai/ (the canonical download landing), and the
remaining href="#" closing links (Star on GitHub / Discord / X /
See All Agents) point to the repo, Discord invite, and X profile.
Also split the landing-deck primary CTA off the repo-root URL so the
primary and secondary actions no longer share a destination.

* style(official-decks): anti-slop visual pass across the official deck seeds

Restyle/polish sweep over all 55 official example decks (taste-skill +
impeccable + gsap-skills guidelines):

- A-class seeds (AI-slop look: default Inter, purple-blue gradients,
  uniform rounded shadow cards) fully restyled with distinct typographic
  systems, locked single accents, and off-white/off-black grounds.
- B-class seeds (clear style family, e.g. the zhangzara-* craft set)
  kept their family signature and received polish only: spacing rhythm,
  contrast to WCAG AA, hairline discipline, reduced-motion guards.
- Motion limited to transform/opacity with prefers-reduced-motion
  guards; signature ease cubic-bezier(.16,1,.3,1).
- Content is unchanged: all copy, numbers (60K+ stars, 217+ plugins,
  129 design systems, 21 agents, -80% cost), links, slide counts and
  the deck engine scripts are byte-identical to the previous seeds.
- html-ppt-zhangzara-pin-and-paper already met every rule (craft fonts,
  single ink accent, static content) and is intentionally untouched.

* i18n(official-decks): translate all Chinese deck seeds to English

All official slide seeds are now English-only per template policy.
15 decks translated (~12,000 CJK chars): presenter-mode-reveal,
testing-safety-alert, xhs-pastel-card, tech-sharing,
obsidian-claude-gradient, knowledge-arch-blueprint, xhs-white-editorial,
hermes-cyber-terminal, xhs-post, dir-key-nav-minimal,
graphify-dark-graph, ppt-keynote, guizang-editorial,
swiss-international, open-slide-canvas.

- Layout, engine scripts, slide counts, stats (60K+/300+/217+/129/21/
  -80%) and links unchanged; only content language changed.
- CJK-specific Google Fonts (Noto Sans/Serif SC, ZCOOL KuaiLe, Songti)
  replaced with Latin families matching each deck's voice (Inter,
  IBM Plex Sans, Nunito, Patrick Hand, Georgia, Noto Serif).
- The xhs-* social decks keep their Xiaohongshu aesthetic with native
  English social copy.
- Overflow from longer English copy fixed with minor per-element type
  sizing; every deck screenshot-verified; zero CJK characters remain
  across all 56 official deck seeds.

* fix(xhs-pastel-card): restore absolute positioning of topbar/footer chrome

The content-layer rule '.slide > *:not(.xp-blob){position:relative}'
(specificity 0,3,0) overrode the absolutely-positioned .xp-topbar and
.xp-footer (0,2,0), dropping both into the flex flow: the chip overlapped
the headline, left:90px turned into a 90px rightward shift that pushed
the page number off-canvas, and bottom:40px lifted the footer into the
body copy on every slide. Excluding the two chrome elements from the
content-layer rule restores the intended layout; verified per-slide
screenshots across all 8 slides.

* feat(deck-seeds): expand the three sampler decks to full 10-page examples

deck-guizang-editorial, deck-swiss-international, and
deck-open-slide-canvas shipped as 1-2 page style samplers while every
other official seed carries a full 8-11 page example. Each is now a
10-page deck built from its own SKILL.md layout vocabulary:

- guizang-editorial: one page per L01-L10 layout, single Ink/Monocle
  palette per the 5-pick-1 rule, ink/paper reversal for rhythm.
- swiss-international: 10 of the 22 locked layouts (statement, cards,
  six-cell grid, timeline, duo compare, loop diagram, why-now, closing
  manifesto), Klein Blue as the single accent; SKILL.md example_desc
  updated from the stale two-page wording.
- open-slide-canvas: ten free-composition 1920x1080 canvases (asymmetric
  split, oversized numerals, positioned-block flow, editorial quote,
  comparison spread) with keyboard + hash navigation per its own
  convention.

Content follows the English-only OD narrative with canonical stats and
links; every page screenshot-verified; zero CJK; single-file with
Google Fonts as the only external resource.

* fix(decks): wire the two remaining dead closing-slide CTAs

Review follow-up for #4145 (nettee, looper reviewer):

- 8-bit-orbit: the closing 'Download Open Design' / 'Star on GitHub'
  pixel buttons were plain <button> elements with no click handling;
  converted to anchors targeting https://open-design.ai/ and the
  GitHub repo, keeping the .pixel-btn skin.
- retro-windows: the closing copy promised download/star actions but
  the action row was the old unwired Restart / Contact / End Session
  set; replaced with Download Open Design / Star on GitHub /
  Join Discord anchors in the same .btn-retro skin.

* fix(decks): wire remaining button-skinned dead CTAs across deck seeds

Review follow-up for #4145 (looper reviewer round 3). Converts every
remaining action-copy control that carried button/chip skin but no
href into a real anchor with the canonical targets, keeping each
deck's skin:

- zhangzara-long-table: closing .pill chips (GitHub / Download) and the
  cover 'Star on GitHub' pill
- zhangzara-raw-grid: closing .label arrow Download + the two
  .s10-rb-block tiles (macOS-Windows / Star on GitHub)
- zhangzara-peoples-platform: GET STARTED slide DOWNLOAD block
- obsidian-claude-gradient: all four .oc-pill chips on the CTA slide
- zhangzara-biennale-yellow / sakura-chroma / cobalt-grid colophon
  ftag labels, zhangzara-pink-script 'Star on GitHub' tile, and
  ib-pitch-book 'Download Open Design' stamp (same pattern, found by
  a repo-wide sweep for the regression class)

* fix(decks): wire zhangzara-coral closing-slide social icons to anchors

* feat(plugins): unify guizang-ppt (magazine-web-ppt) deck default with the Open Design product narrative in English

The deck-mode default seed (magazine-web-ppt, dir guizang-ppt) was the one
official deck the narrative+i18n pass missed, because its content lives in
assets/example-slides.html + assets/template.html rather than example.html.

- Rewrite the 9-slide example deck to the English Open Design product narrative
  (60K+ stars / 300+ contributors / 217+ plugins / 129 design systems / 21
  agents / -80% cost), preserving the magazine editorial layout vocabulary and
  hitting all 8 layout categories. English titles use the Playfair .h-hero-en
  family; copy follows anti-slop craft (no decorative em-dashes, real labelled
  metrics, tight hierarchy).
- Translate template.html (title, hint, all CSS/JS comments), SKILL.md,
  references/*.md, open-design.json, and the design-templates README to English.
- Keep magazine-web-ppt name, mode=deck, default_for=deck intact.
- Both synced copies (plugins/_official + design-templates) updated identically.

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
2026-06-16 08:41:04 +00:00
a0afc584bb [codex] centralize daemon data directory docs (#4222)
* docs: centralize daemon data directory contract

* fix(e2e): allow slower artifact consistency navigation

Generated-By: looper 0.9.5 (runner=fixer, agent=codex)

* docs: localize daemon data directory pointers

Generated-By: looper 0.9.5 (runner=fixer, agent=codex)

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-06-15 02:52:05 +00:00
ec524986b5 chore(plugins): remove html-ppt-dir-key-nav-minimal and html-ppt-xhs-post (#4212)
* chore(plugins): remove html-ppt-dir-key-nav-minimal and html-ppt-xhs-post

Final cut list from the maintainer review of the slide-template gallery
(narrowed from the originally proposed nine html-ppt studio seeds to
these two; the other seven stay).

Removed across every surface:
- plugins/_official/examples/<slug>/ (2 plugin dirs)
- design-templates/<slug>/ (2 catalogue entries)
- plugins/registry/official/open-design-marketplace.json entries
- apps/web/src/i18n/content{,.ru,.fr}.ts slug keys

Landing-page solution showcases never referenced these two, so no
landing changes are needed. pnpm guard and pnpm typecheck pass;
repo-wide grep for both slugs returns zero hits.

* fix(registry): sync bundledPreinstallCount after removing two plugins

plugins-marketplaces.test.ts asserts metadata.bundledPreinstallCount
equals the registry plugins array length; update 414 -> 412 to match
the two removed entries.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-13 13:15:04 +00:00
01d8e6baab feat(plugins): 23 community slides/deck plugins from top open-source slides skills (#4127)
* feat(plugins): add 30 community slides/deck plugins from top open-source slides skills

Convert the highest-starred MIT-licensed slides skills into community deck
plugins (one template style = one plugin), plus 3 family root plugins:

- frontend-slides (zarazhangrui, 21.1k stars): root + 7 style variants (fs-*)
- huashu-design (alchaincyf, 18k stars): slides cut root + 8 variants (huashu-*)
- html-ppt-skill (lewislulu, 5.8k stars): root + 9 theme variants (hps-*)
- visual-explainer (nicobailon, 8.7k stars): 3 aesthetics (ve-*)

Each plugin ships the standard trio (open-design.json + SKILL.md locked spec +
self-contained example.html seed, zero external JS), validated against
PluginManifestSchema. All 30 registered in the community marketplace registry
(0.1.2 -> 0.2.0). Every variant was deduped against the 59 existing official
deck plugins (zhangzara ports, guizang, html-ppt family) before inclusion.
Upstream attribution preserved: MIT LICENSE files + author/homepage per plugin.

* feat(plugins): unify all 30 community deck seeds with the Open Design product narrative

Replace per-deck placeholder content with one canonical 10-section story
sourced from the homepage structure doc and README: cover, why (Claude
Design proved it, OD opens it), three claims, numbers board (60K+ stars /
300+ contributors / 217+ plugins / 129 design systems / 21 agents), 5-step
workflow, multimodal outputs, capabilities, agent ecosystem, testimonial,
and CTA with GitHub/Discord/X links for organic resharing.

Content-only edits: CSS tokens, layouts, decorations, animations, keyboard
nav and hash routing untouched in every deck. huashu-* decks carry the zh
copy; fs-/hps-/ve- decks carry the en copy (latin display faces). All 29
rewrites + 1 pilot verified: script syntax, cover screenshot, no fictional
brand residue.

* fix(ve-presets): align preview motion and slideCount with example contracts

Review follow-up for #4127 (mrcfps):
- ve-midnight-editorial / ve-terminal-mono / ve-warm-signal set
  od.preview.motion to "deck" while their example.html files are
  vertical scroll-snap documents; switched to "scroll" so the
  marketplace hover clip traverses them correctly.
- ve-terminal-mono / ve-warm-signal advertised slideCount options
  (5-10 / 10-20 / 20+) that the locked prompt overrode with a hard
  8-11 requirement; the prompt now honors {{slideCount}}, and the
  SKILL.md slide-count rule is rephrased as a default that yields to
  the user's selection.

* style(community-slides): anti-slop polish across the 30 community deck seeds

Visual polish sweep (taste-skill + impeccable + gsap-skills guidelines)
over every community slides plugin example:

- All theme tokens stay LOCKED to each plugin's od.useCase.query spec;
  polish is limited to spacing rhythm, contrast (WCAG AA), hairline
  discipline, and prefers-reduced-motion guards.
- Motion only via transform/opacity; engine scripts byte-identical.
- Content unchanged: copy, numbers, links and slide counts match the
  previous seeds; SKILL.md craft notes updated where the polish touched
  documented details.

* fix(fs-presets): honor requested slideCount in fs-dark-botanical and fs-notebook-tabs

Review follow-up for #4127 (mrcfps, round 2): both plugins offered
medium/long slideCount options in their pickers while the en query and
SKILL.md hard-locked 8-11 slides, so picking 'medium 12-16' or 'long'
still produced an 8-11 deck. Same contract fix as the ve-* presets in
c2acb0c86:

- open-design.json en query now says 'match the requested slide count
  ({{slideCount}})' instead of a hard 8-11.
- SKILL.md states 8-11 as the default and honors the user's pick.

* i18n(community-slides): translate all Chinese deck seeds to English

All slide plugin example seeds are now English-only per template policy:
the 9 huashu-* decks (~7,200 CJK chars), html-ppt-studio and
hps-y2k-chrome (CSS-comment-level residue) are fully translated.

- Theme tokens, engine scripts, slide counts, stats (60K+/300+/217+/
  129/21/-80%) and links are unchanged; only content language changed.
- CJK-specific Google Fonts swapped to their Latin equivalents where
  the translated text used them (Noto Serif/Sans SC -> Noto Serif/Sans
  in huashu-takram-soft-tech and huashu-slides); locked token stacks
  untouched elsewhere.
- Minor type-size adjustments where English copy ran longer than the
  Chinese original (huashu-annual-letter prose/quote).
- Every deck screenshot-verified for overflow; zero CJK characters
  remain in any community example.html.

* chore(community-slides): drop four plugins per design review

Remove fs-bold-signal, hps-aurora-glass, html-ppt-studio, and
ve-warm-signal from the community slides batch — cut in the maintainer
design pass over the full template gallery (fs-bold-signal duplicates
the frontend-slides root look; hps-aurora-glass and html-ppt-studio
read closest to generic gradient decks; ve-warm-signal overlaps the
other two ve-* presets).

- Their marketplace entries are removed from
  plugins/registry/community/open-design-marketplace.json.
- hps-true-blueprint SKILL.md no longer points at the html-ppt-studio
  root plugin as if it shipped in-repo; it now references the upstream
  system the hps family is ported from. The remaining hps-* decks are
  self-contained single files and unaffected.

* chore(community-slides): drop fs-dark-botanical per design review

Second cut from the maintainer gallery pass. Plugin directory and its
marketplace registry entry removed; no other references existed.

* chore(community-slides): drop hps-catppuccin-mocha and hps-vaporwave per design review

Third cut from the maintainer gallery pass. Plugin directories and their
marketplace registry entries removed; no other references existed.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-12 09:17:58 +00:00
08a2bdc959 Add /deep-think — Maximum Cognitive Effort Protocol plugin (#4116)
* Add /deep-think — Maximum Cognitive Effort Protocol plugin

* feat: make /deep-think plugin portable and add name to frontmatter

* fix(manifest): clean up serialized YAML description to a plain sentence

* chore: sanitize references in deep-think plugin

- Removed internal provenance.json to prevent metadata leakage
- Replaced private workspace instructions in source.md with a clean attribution note

* chore: remove reference to mallettile.ca in attribution note

* fix(plugin): add deep-think agent skill metadata

* fix(plugin): remove workspace-specific deep-think prompt

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
2026-06-11 14:36:11 +00:00
2940876409 feat(plugins): refresh HyperFrames video templates from html-video (#4063)
* feat(plugins): refresh HyperFrames video templates from html-video

Replace the 11 HeyGen catalog stub entries (no bundled source, duplicate
catalog-block cover images) with 23 fully bundled HyperFrames templates
imported from nexu-io/html-video, so both the in-app Plugins home and the
marketing site's /plugins/templates/hyperframes/ show real, renderable,
visually distinct templates with hover-autoplay motion previews.

Added (plugins/_official/video-templates/<slug>/):
- open-design.json — od.surface "hyperframes" (HyperFrames artifact type),
  en + zh-CN title/description, inputs, and jsDelivr-hosted preview assets.
- template.json — client card metadata.
- SKILL.md — how to repaint and render the composition.
- source/ — the HTML+CSS+GSAP (or Remotion) composition, verbatim.
- preview.mp4 — a looping motion preview rendered with the html-video
  HyperFrames renderer, trimmed to the full animation length (cards
  hover-autoplay from frame 0, detail page plays it); each under the 1 MiB
  blob limit. 21 of 23 ship a video; frame-logo-outro (a held logo) and
  frame-data-rollup (Remotion, blank native render) ship a still poster.
- preview.png — poster frame extracted from that render.

Landing-page tweaks:
- Hide the two cover-less examples/{hyperframes,video-hyperframes} onboarding
  scenarios from the public template library (feature demos, not templates).
- Point the "use this" CTAs (template card + plugin detail) at the desktop
  download page instead of the detail page / GitHub releases.
- Hover-autoplay restarts each preview from frame 0 so the full animation
  plays on every hover instead of resuming mid-loop.
- SEO: keyword-led <title> ("HyperFrames templates & skills …") and a
  rewritten, keyword-rich category description (all 18 locales) to target
  "hyperframes template" / "hyperframes skill" search intent.

Removed the 11 source-less hyperframes-* HeyGen catalog stubs. Licensing is
mirrored faithfully per upstream and summarised in HYPERFRAMES-ATTRIBUTIONS.md:
8 Apache-2.0 forks of heygen-com/hyperframes, 1 MIT from
nateherkai/hyperframes-student-kit, 7 original Apache-2.0, and 7 derived from
MIT design skills (frontend-slides, huashu-design) with studio inspiration
recorded as non-affiliated L1 origin.

Registry seed nets +12 entries (402 -> 414); bundledPreinstallCount updated.

* fix(plugins): self-contain hyperframes templates + narrow aspect options

Address review on the HyperFrames import:
- Narrow each fixed-layout (1920×1080) hyperframes manifest's
  od.inputs.aspect.options to the single aspect its template.json actually
  ships (e.g. frame-vignelli is 9:16, the rest 16:9), so the picker/agent
  can't request portrait/square renders the composition can't produce.
  frame-data-rollup (responsive Remotion, layout computed off canvas size)
  keeps 16:9/9:16/1:1.
- frame-kinetic-type: drop the external 9 MB S3 A-roll video from
  source/compositions/main-graphics.html. The timeline sets that layer to
  opacity:0 at t=0 and never reveals it, so it was never visible; replacing
  it with an empty self-contained layer keeps the render identical (verified)
  and removes the only third-party runtime media dependency. Re-rendered its
  preview from the vendored source.

* fix(plugins): vendor all runtime assets so hyperframes templates are self-contained

Make every bundled HyperFrames template render with no third-party network
dependency, per review and per each template's own SKILL.md "keep the
composition self-contained under source/" contract:

- GSAP: vendor gsap@3.14.2 into each template's source/ and rewrite the
  jsDelivr <script src> to a relative path (9 templates).
- Google Fonts: fetch each family's woff2, ship them under source/fonts/,
  inline a local @font-face fonts.css, and drop the googleapis <link>/@import
  (20 templates). CJK families (Noto Sans/Serif SC) are intentionally not
  vendored — they exceed the 1 MiB blob limit; their placeholder sample text
  falls back to the OS CJK font, keeping the source dependency-free.
- Tailwind: replace the cdn.tailwindcss.com JIT script with a static
  tailwind.css built from the classes each template actually uses (6).
- Textures/media: vendor the remaining external texture (warm-grain) and copy
  the previously-missing local assets/ (screenshots, audio, swiss-grid.svg)
  into frame-product-promo, frame-product-promo-30s, frame-swiss-grid.
- od.context.assets now stages ./template.json + ./source so the renderer
  materializes the full bundle.

All vendored files are under the 1 MiB blob limit. Re-rendered the six
templates whose output could shift (Tailwind static build + CJK fallback);
verified renders are visually intact. Final audit: zero external CDN/http
asset references across all 23 template sources.

* fix(plugins): inline vendored GSAP + enrich plugin detail SEO

- Inline the vendored GSAP runtime directly into each template's index.html
  instead of shipping gsap.min.js, so no new committed `.js` files trip the
  repository-layout guard (`scripts/guard.ts` residual-JS policy). Compositions
  use the global `gsap` the inlined runtime defines. Templates stay fully
  self-contained; re-rendered frame-kinetic-type / frame-swiss-grid to confirm
  motion is intact.
- Plugin detail pages now lead their <title> and meta description with the
  artifact type + brand keywords — "<HyperFrames video template> · Open Design"
  and "... for Open Design — the open-source Claude Design alternative" — so
  each detail page ranks for "hyperframes template", "Open Design", and
  "open source Claude Design alternative".

* fix(plugins): replace placeholder screenshots in frame-product-promo-30s

The product-surfaces scene referenced source/assets/screenshot-{1,2,3}.png,
which were 67-byte 1×1 placeholder PNGs carried over from the upstream
template, so a render of that scene showed blank laptop screens. Replace them
with three self-contained 1440×900 dashboard mockups (distinct Plan/Build/
Monitor accents) so the bundled source renders usable product surfaces, and
re-render the preview. All three are well under the 1 MiB blob limit.

* fix(plugins): give frame-logo-outro a motion preview like the rest

frame-logo-outro shipped as an image-only preview, so it was the one new
HyperFrames template without hover-autoplay / detail playback. Re-render it
from its (now self-contained) source as a short looping logo-reveal MP4 and
switch its manifest to previewType "video", matching the other 22. 72 KB,
under the 1 MiB blob limit. (frame-data-rollup remains a still poster — its
Remotion native render comes out blank; tracked as a follow-up.)

* fix(landing): keep plugin detail lead localized, SEO suffix meta-only

The keyword-rich English SEO suffix was appended to `description`, which is
both the <meta description> and the visible <p class="lead">, so non-English
plugin detail pages rendered mixed-language body copy. Split it: `description`
stays the fully-localized lead, and a separate `seoDescription` carries the
English brand/keyword suffix for <Layout description> + JSON-LD only.

---------

Co-authored-by: Joey <276262049+xne998808-ai@users.noreply.github.com>
2026-06-11 09:17:17 +00:00
b034609cde chore(plugins): remove the shamoni example plugin (duplicate preview imagery) (#4040)
* chore(plugins): remove the shamoni example plugin

The Shamoni scroll-driven gallery example ships a baked preview that
duplicates luxury-botanical's hero imagery, so the Community shelf
shows the same perfume tile twice. Curator call: drop the example
entirely — plugin folder, baked-preview manifest entry, and its slot
in the pinned curated ordering.

* fix(daemon): prune persisted bundled rows when their folder leaves the image

The bundled boot walker only upserted folders that still exist, so a
plugin removed from the daemon image (like the Shamoni example this PR
deletes) survived in upgraded installs' installed_plugins table — and
/api/plugins kept serving a record whose backing files were gone.

Make bundled rows mirror the bundled tree: after a successful walk,
delete source_kind='bundled' rows whose folder was not seen this boot.
Folders that still ship but fail to parse stay registered (warned, not
pruned), the ENOENT early-return never prunes (a missing bundledRoot is
a packaging bug, not a removal), and user-installed rows are untouched.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-10 08:07:12 +00:00
ec47346e94 feat(web): composer 插件菜单 & 设计百宝箱 hover 预览 (#4000)
* feat(web): hover-preview column in the composer plugins menu

Hovering (or arrow-defaulting to) a plugin in the composer "+" → Plugins
flyout now opens a second-level preview column: a live preview hero
(reusing the plugins-home PreviewSurface — media poster or sandboxed
example iframe) plus the localized title, trust badge, description, and
tags. The previewed row stays highlighted so the panel's subject is
clear before the cursor lands.

The Plugins flyout widens to fit the side column and reserves that width
in the placement math; on narrow (contained) surfaces the preview drops
below the list instead. Other submenus are unchanged.

* feat(web): design-toolbox hover preview + styled detail panel

Extends the plugin hover-preview to the project composer's design
toolbox. Hovering a plugin entry now renders the same rich preview
(ComposerPluginPreview — poster / sandboxed example iframe + title,
trust badge, description, tags); every other kind (skill, MCP,
connector, file, follow-up action) keeps a compact text card.

The shared hover-detail panel (.plus-menu__detail) previously shipped
with no CSS at all — it had no position/background/border, so the
inline left/top never applied. This adds the panel styling (fixed,
panelled, shadowed, viewport-clamped) plus the section-label and
detail text styles, and clamps the panel's top so the taller plugin
preview stays on screen. ComposerPluginPreview's column divider was
de-scoped to the plugins-flyout context so it renders cleanly inside
the toolbox panel too.

* fix(web): drop redundant divider above design-toolbox search

The toolbox head carried a border-bottom while the search box below it
has its own full border, so with the flyout gap the two hairlines read
as a doubled line. Drop the head border; the search box already bounds
the section.

* fix(web): localize plugin titles in the composer plugins list

The list rendered the raw `plugin.title` (often English) while the
hover preview used the localized `title_i18n`, so the same plugin showed
two different-language names side by side. Render the localized title in
the list too, and fold it into the search haystack so a localized-name
query still matches.

* feat(web): show plugin kind tag in the hover preview

Adds a localized kind pill (Prototype / Slides / Image / Video / Audio /
HyperFrames / Live artifact) to the plugin preview title row, reusing
extractCategories — the same taxonomy that powers the home Community
filter chips — so the preview names the plugin's type at a glance. Shows
in both the composer plugins menu and the design-toolbox preview.

* fix(web): localize the applied-plugin chip title

The composer's applied-plugin chip showed the raw `record.title` (English,
e.g. "Audio Jingle") while the same plugin's preset card / hover preview
used the localized `title_i18n` (e.g. "音频铃声"), so the two read in
different languages. Localize the chip in both surfaces:
- HomeView: activeBadgeTitle now uses localizePluginTitle(locale, record).
- PluginsSection: the ContextItem label (project ChatComposer chip) too.

Mirrors the already-correct PluginPromptPresetCard pattern.

* fix(web): localize trust badge, drop raw tag chips from preview

- TrustBadge now resolves its label via i18n (pluginsView.trust.*) so
  Official / Trusted / Restricted follow the active locale everywhere it
  renders; an explicit `label` prop still overrides.
- Drop the raw manifest tag chips from the plugin hover preview. Tags are
  author-defined English slugs with no controlled vocabulary, so they
  can't be reliably localized; the localized kind pill already conveys
  the category, so the card stays single-language.

* feat(web): tag design-system plugins in the hover preview

Design-system plugins aren't one of the home Community facets, so
extractCategories returns nothing and they had no kind pill. Detect them
by mode/tag (same heuristic as the preview classifier) and show the
localized 'Design systems' label (entry.navDesignSystems).

* i18n(plugins): backfill title/description translations for 22 example plugins

These 22 official example manifests shipped title_i18n / description_i18n
with only zh-CN + en, so in every other UI language (Korean, Japanese,
…) the plugin name and description fell back to English — visible now
that the composer surfaces them in the hover preview.

Fill all to the full 18-locale set (zh-CN, zh-TW, ja, ko, de, fr, ru,
es, pt-BR, it, vi, pl, id, nl, ar, tr, uk, en), matching the locale set
used by the already-complete manifests. Brand/product/proper names and
technical terms are kept verbatim; useCase.query stays English-authoritative.

* fix(web): clamp design-toolbox detail panel into the viewport

The left-side fallback (when the right side overflows) used
rect.left - gap - detailWidth without clamping, so a row near the left
edge or a pane narrower than detailWidth + gap*2 produced a negative
left and pushed the position:fixed panel off-screen. Clamp the chosen
left into [8, innerWidth - 8 - detailWidth] so it always degrades
gracefully. (review: nettee/looper)

* test(web): extract + unit-test toolbox detail panel positioning

Pull the detail-panel placement out of showToolboxDetail into a pure
computeToolboxDetailPosition helper and pin the narrow-pane clamp with a
focused vitest regression (row near left edge / viewport narrower than
the panel never yields a negative left; top clamps too). Addresses the
nettee/looper review ask for a regression check.

* fix(web): match plugins-flyout placement reserve to rendered width

PLUS_MENU_PLUGIN_FLYOUT_WIDTH was 560 while .plus-menu__flyout--plugins
renders at 466px, so getFlyoutPlacement over-reserved and medium-width
panes (~664-757px side room) wrongly fell back to the contained layout,
silently dropping the side-by-side preview column. Set it to 466 (with a
cross-reference comment to the CSS), extract the boundary arithmetic into
a pure resolveFlyoutSide helper, and pin the 466-vs-560 boundary with a
vitest regression. (review: nettee/looper)

* fix(web): localize trust badge tooltip + aria text

title and aria-label still came from the hard-coded English
meta.description, so on non-English locales the badge read as
mixed-language to tooltips and screen readers. Resolve all of label,
title, and aria-label from the localized tier key. (review: nettee/looper)

* fix(web): update TrustBadge test for localized a11y text

The existing TrustBadge spec asserted the old hard-coded English
descriptions ("Open Design official", aria "…: Action plugin"), which the
localization change replaced — that's why Web workspace tests went red.
Keep a localized tier prefix in the accessible text when a contextual
label is passed ("Official: Action plugin") and update the spec to pin
the localized behavior.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-09 17:04:11 +00:00
28f5516697 feat(prompt-templates): add desk hologram AR realdesk Seedance 2.0 video template (#3967)
* feat(prompt-templates): add desk hologram AR realdesk Seedance 2.0 video template

Ship the two-phase keyframe + i2v workflow behind blue-warrior-realdesk-t2v-15s
as a first-party video prompt template with preview MP4/poster and plugin bundle.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(prompt-templates): host desk-hologram previews on repo-assets CDN

Drop the 8.9 MB tracked MP4 to satisfy the 1 MiB blob guard and point
previewImageUrl/previewVideoUrl at repo-assets.open-design.ai, matching
the Three Kingdoms Seedance templates. Bump bundledPreinstallCount to 402.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: add workflow to publish prompt-template previews to repo-assets R2

Maintainer workflow_dispatch helper for template PRs that drop large MP4s
from git and point preview URLs at repo-assets.open-design.ai.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: auto-publish desk-hologram template previews on workflow push

Run the repository-assets upload when this workflow lands on the
template branch so PR #3967 preview URLs go live without waiting for main.

Co-authored-by: Cursor <cursoragent@cursor.com>

* ci: keep repository-template asset publish as workflow_dispatch only

Assets for PR #3967 are live on repo-assets; drop the one-shot branch push
trigger now that the upload succeeded.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(plugins): add specVersion to desk-hologram video template manifest

Align open-design.json with the official video-template plugin schema.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 12:16:49 +00:00