feat(design-systems): add structured runtime contract - #6456
Conversation
|
@app/open-design-crew I'm holding off on generating review comments for #6456 because this pull request has merge conflicts right now. Please resolve the conflicts with main and push the updated branch. Once that's done, request or wait for the review to run again and I'll take another look. π Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos. |
β¦t-fix # Conflicts: # apps/daemon/src/design-systems/index.ts
π Landing page previewThis PR is deployed to a Cloudflare Pages preview β not staging or production:
|
|
π§ͺ This PR has changes that need a manual QA pass before merge. Please hold off merging for now; we'll loop QA in once it's merge-ready. |
|
Queued manual QA for the structured runtime contract before merge. @ivy-ting has been requested to validate the design-system runtime loading, manifest guard, and intent-resolution flow on this head. |
|
@open-design-crew Thanks for the contribution. I completed QA validation for this PR. QA Acceptance Record Scope:
Verified:
Not verified:
Risks / notes:
Conclusion:
|
* feat(design-systems): add structured runtime contract (nexu-io#6456) * feat(design-systems): add structured runtime contract * chore: retrigger CI for PR 6456 --------- Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com> Co-authored-by: mrcfps <mrc@powerformer.com> * feat(design-systems): resolve structured runtime intents * fix(design-systems): preserve workspace runtime scope * test(design-systems): cover scoped prompt runtime * fix(design-systems): use one component authority * fix(design-systems): preserve run workspace authority * fix(design-systems): honor runtime rollback gate * fix(design-systems): pin run binding authority * fix(runs): reject client-supplied scopes * fix(design-systems): keep user package assets scoped * feat(design-systems): validate generated UI adherence * test(e2e): assert durable reload authority state * fix(design-systems): reject non-executable adherence evidence * fix: harden design system adherence validation * fix(design-systems): validate JSX selectors structurally * fix(design-systems): close adherence input gaps * chore(nix): refresh pnpm dependency hashes * chore(ci): retrigger design system validation * fix(daemon): bound design system artifact reads * fix(design-systems): bridge Codex runtime tools * docs(codex): clarify shell environment policy --------- 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: mrcfps <mrc@powerformer.com>
β¦conformance
* fix(web): recover stalled iframe preview refreshes (#6714)
* fix(web): auto-open generated media previews (#6719)
* fix workspace deletion and extension installation failures (#6701)
* fix(workspace): reduce project and extension failures
* fix(workspace): avoid remote checks for local project deletes
* fix(workspace): address extension install review gaps
* fix(skills): retry manifestless ref prefixes
* fix(plugins): close remaining install diagnostics gaps
* fix(plugins): preserve user-input error buckets
* test(plugins): enforce bounded cleanup diagnostics
* fix(plugins): classify manifest validation failures
* feat(analytics): report the Share tab publish flow (publish_file / artifact_publish_result / copy_publish_link) (#6651)
* feat(analytics): report the Share tab publish flow
The 0.18.0 unified Share panel made 'Publish this file for everyone' the
primary sharing path, but the whole chain was untracked: no click event,
no outcome event, no copy-link event. This wires the three points:
- ui_click element 'publish_file' when the publish button is clicked
- artifact_publish_result (new event, mirroring the artifact_deploy_result
split) when a publish/unpublish attempt settles, with action/result/
error_code/publish_duration_ms
- ui_click element 'copy_publish_link' on the post-publish copy button
Both viewer copies (ReactComponentViewer, HtmlViewer) report the same
shape; ReactComponentViewer has no projectKind prop so it reports
project_kind null. Failure codes are stable analytics values
(workspace_identity_required | publish_failed), not i18n keys.
* fix(analytics): gate HtmlViewer publish tracking on workspaceActive and close the error_code type
- Wrap the new publish/copy-link/result emissions in HtmlViewer behind
fire*-style helpers that early-return on !workspaceActive, matching every
neighboring emission site; the publish/unpublish network calls stay
unconditional. ReactComponentViewer has no workspaceActive prop and is
unchanged.
- Declare TrackingPublishErrorCode as a closed two-value union in
contracts shared-enums, use it for ArtifactPublishResultProps.error_code,
and import it from publish-error-code.ts so the helper and contract
cannot drift.
- Extend the publish-tracking suite with unpublish success/failed cases and
a ReactComponentViewer case asserting project_kind: null.
Generated-By: looper 0.11.8 (runner=fixer, agent=claude-code)
* fix(analytics): report the real project kind from ReactComponentViewer publish events
ReactComponentViewer's publishTrackingIdentity() hard-coded
project_kind: null, so every publish_file, copy_publish_link, and
artifact_publish_result emitted for a React artifact lost project-kind
attribution β even though FileViewer requires Props.projectKind and had
the value in scope at the render site.
Thread projectKind into ReactComponentViewer and return it from
publishTrackingIdentity(), matching the HtmlViewer copy so both viewers
really do report the same shape.
The React-path test now asserts project_kind 'prototype' on both the
publish click and the publish result, and additionally covers the
copy_publish_link click that previously had no React-path assertion.
Generated-By: looper 0.11.8 (runner=fixer, agent=claude-code)
* fix(analytics): read the live workspaceActive ref when a publish settles
firePublishResult closed over the workspaceActive prop captured by the
render that created the click handler. A publish/unpublish request can
start while the HtmlViewer is active and settle after the user switches
tabs; the in-flight continuation still saw the render-time `true`, so a
retained (inert) viewer emitted artifact_publish_result anyway β exactly
what the gate added in ff77c14 promised it would not do.
Read workspaceActiveRef.current, the live value the component already
maintains, instead of the captured prop. firePublishFlowClick keeps the
prop: it only runs inside a synchronous click handler, where the two
values cannot diverge.
Regression test drives a gated publish request, rerenders the viewer with
workspaceActive: false, then settles the request and asserts no
artifact_publish_result is recorded. Verified red on the prop-reading
version and green on this one.
Generated-By: looper 0.11.8 (runner=fixer, agent=claude-code)
* fix(analytics): derive publish artifact_kind from the renderer id
Both publishTrackingIdentity() helpers passed only file.kind to
artifactKindToTracking. That mapper recognises HTML solely through the
renderer id β file.kind 'html' (HtmlViewer) and 'code'
(ReactComponentViewer) both fall through to 'unknown'. Every new
publish_file, copy_publish_link, and artifact_publish_result therefore
shipped artifact_kind: 'unknown', so the funnel this PR exists to create
could not be segmented by artifact kind at all. QA confirmed the same
'unknown' payloads reaching the test ingest sink.
Supply the renderer identity each helper already knows statically:
ReactComponentViewer is only rendered on the 'react-component' renderer
match, and HtmlViewer is only rendered on the 'html' / 'deck-html'
matches β which is exactly what its isDeck prop is derived from. Both now
map to 'html'.
Scoped to the two helpers this PR added; the pre-existing fileKind-only
call sites elsewhere in FileViewer are left untouched.
Assertions for artifact_kind: 'html' added to the HTML and React paths in
FileViewer.publish-tracking.test.tsx. Verified red without the source
change (both cases report 'unknown') and green with it; full web suite
6309 passed, pnpm guard and pnpm typecheck clean.
Generated-By: looper 0.11.8 (runner=fixer, agent=claude-code)
* test(analytics): follow the Share panel's publish row after the header split
#6654 removed the Share popover's tab strip and relabelled the publish
entry: the row is now a `role="menuitem"` labelled by
`fileViewer.publishSingleFileTitle` ("Get a share link", was "Publish this
file for everyone"), and the unpublish control reads "Stop sharing" (was
"Unpublish file"). The publish-tracking suite still drove the pre-split
labels, so every case failed to find its control after the rebase.
Only the queries move β which control fires which event is unchanged, and
the payload assertions are untouched.
* fix(analytics): gate ReactComponentViewer publish results on the live active view
`FileWorkspace` keeps a non-active viewer mounted and re-renders it with
`workspaceActive={false}`, but `FileViewer` never forwarded that prop to
`ReactComponentViewer`. A React publish/unpublish started while the tab was
active and settling after the user switched away therefore still reported
`artifact_publish_result` from the hidden viewer, attributing the outcome to
whatever tab the user had moved to.
Thread `workspaceActive` into `ReactComponentViewer` and route both click and
result emissions through the same helper pair HtmlViewer already uses: clicks
read the render-time prop, results read a live ref so the in-flight
continuation sees the current value rather than the one it captured.
Red spec: the new `(ReactComponentViewer)` case in
`FileViewer.publish-tracking.test.tsx` β it fails on the previous source
(`expected [ { page_name: 'artifact', β¦ } ] to deeply equal []`) and passes
with this change.
* test(analytics): wait for the publish panel to settle instead of timer turns
The inactive-viewer regression released `publishGate` and then assumed two
`setTimeout(0)` turns were enough for the handler continuation to run before
the negative assertion β turns that are not tied to the operation, so a
different scheduler could let the assertion win the race and pass falsely.
Wait for the operation's own completion signal instead. `publishingPublicFile`
is cleared in the handler's `finally`, strictly after the point where the
result event would have been emitted, so the panel leaving its "Creating linkβ¦"
state proves the continuation ran past the emission site. A retained viewer
renders no chrome, so the assertion switches back to active first β the
inactive window has already closed by then, and an event emitted during it
would still have been recorded.
Settled looks different per chrome, and both shapes are accepted: HtmlViewer
discards the published URL at the request-identity guard that follows the
emission site and returns to the idle publish row, while ReactComponentViewer
commits it and swaps in the copy-link control.
Still red without the source gate (`ReactComponentViewer` case fails in 45ms
with `expected [ { page_name: 'artifact', β¦ } ] to deeply equal []`), green
with it.
---------
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
* feat(landing): show hosted multimodal benefits (#6395)
* feat(landing): show hosted multimodal benefits
Rebased onto main: keeps the hosted multimodal scope (image/video model
rosters, per-plan benefit row, shared Open Design Cloud panel) and drops the
team-card copy tweaks that main's pricing contract now forbids.
* fix(landing): mark hosted video generation as coming soon
Hosted video has no server-owned entitlement or billing path yet, so the
video roster renders muted behind a localized parenthetical instead of as an
included paid benefit. Applies to all 10 pricing locales and to the shared
Open Design Cloud panel.
* fix(landing): mute the coming-soon panel tile consistently
The panel tile used `--ink-soft` (#434343), which sits too close to `--ink`
(#262626) to read as unavailable. Match the mute already used by the
coming-soon model roster so both surfaces agree.
* fix(landing): drop the unbacked hero billing guarantee
The hero subtitle promised that a run with no result never consumes credits
and implied hosted image/video generation is live. Neither has a server-owned
path yet, and the line was outside this PR's scope, so the hero returns to
main's heading-only shape across all 10 locales.
* fix(landing): align the image roster with the shipped Cloud catalogue
`Seedream 5 Lite` is not a Cloud model β the shipped registry publishes
seedream-5.0 and seedream-5.0-pro, so the label now reads `Seedream 5 / Pro`.
Both rosters now cite the daemon registry that decides what a paid plan can
actually reach.
* fix(landing): make the shared panel copy agree with video coming-soon
The benefit row and panel description still said hosted video was ready to
use while the roster and panel tile marked it coming soon. Both strings now
describe agent/image as available and separate video as coming soon, across
all 10 locales.
---------
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
* fix(release): guard automated release trains (#6729)
* fix(release): guard automated release trains
* fix(release): resolve beta recovery before checkout
* style(release): group workflow outputs
* fix(release): publish daily beta snapshots without promotion
* fix(collab): bound the two unbounded vela fan-outs (#6711)
* refactor(collab): extract the team resource listing cache out of server.ts
Pure move, no behavior change: `cachedTeamResourceList` and its scope key
leave the 11k-line server.ts closure for the module that already owns the
listing caches. Nothing about the SWR window, the sync fan-out, or the
invalidation path changes here β the move is what makes the fan-out
testable in the next commit.
* test(collab): pin the vela fan-out concurrency cap (red)
Both collab fan-outs are unbounded today: the peak equals the data size,
not any daemon-owned limit. On this commit the two cap specs fail with
peak=60 (one per shared resource) and peak=40 (one per dirty project).
The three sibling specs β every resource materialized exactly once, a
failing pull still surfacing, every dirty project eventually published β
pass here and must keep passing once the cap lands.
* fix(collab): bound the two vela fan-outs to a daemon-owned cap
Both collab fan-outs were sized by user data rather than by the machine.
Materializing a workspace's shared resources ran one `vela resource pull`
per shared design system / plugin / skill, all at once, on a single
listing read. Publishing ran one `vela resource push` per project whose
debounce expired in the same tick β per-project re-entry was guarded,
across projects nothing was. A workspace that shares 200 resources
therefore decided how many concurrent transfers this daemon opened.
Route both through one `ConcurrencyGate`. Every operation still runs,
exactly once, and still propagates its own failure to its own caller;
only the peak changes. The uncontended path stays synchronous so callers
that observe "the adapter was invoked by the time this returned" β the
run-boundary flush β behave as before.
The scheduler distinguishes queued from reading: a publish waiting for a
slot has not looked at the project yet, so a change arriving in that
window updates the reason it will publish under instead of stacking a
second publish behind it. Once the adapter is reading, the pre-existing
dirty/re-publish path is unchanged.
Cap specs went red at peak=60 and peak=40 on the parent commit.
* fix(collab): size the fan-out cap at 8, matching the hub's own bound
Measured against real child processes rather than promises: a 40-resource
fan-out peaked at 40 concurrent processes before the cap, 4 at cap=4, and
8 at cap=8. The latency cost is real and worth stating β 457ms unbounded,
4235ms at 4, 2152ms at 8 β so pick the bound the hub already puts on its
own per-request fan-out (MAX_BLOB_INSPECTION_CONCURRENCY) instead of a
tighter number neither side would recognize.
* fix(collab): give every resource kind one shared materialization budget
The gate was built inside createTeamResourceListCache, and server.ts
builds three of those caches β design systems, plugins, skills. One
client poll refreshes all three, so each acquired its own eight slots and
the daemon's real peak was cap x 3. The PR claimed a peak of eight; it
was twenty-four.
Move the gate to the composition root and inject it, so the bound is a
property of the daemon rather than of each listing surface. Measured with
real child processes across the three caches: 24 before, 8 after, with
all 60 resources still materialized.
The publish scheduler keeps its own gate: pulls and pushes are opposite
directions with different latency sensitivity, and coupling them would
let a large listing refresh delay a teammate's publish. The daemon-wide
worst case from these two sites is therefore 8 + 8, which is what the
docs now say instead of an unqualified 8.
Reported by PerishCode on #6711.
* docs(collab): state the real daemon-wide peak, not a per-site number
* fix(daemon): authorize spawned-agent exports (#6661)
* fix(daemon): authorize spawned-agent exports
* fix(daemon): authorize nonloopback tool exports
* fix(daemon): stop duplicating form answers on the resume (skipTranscript) path (#6305)
The form-answer transition block already embeds the trimmed currentPrompt,
so appending body = currentPrompt after it on the resume path shipped the
submitted answers twice in the same # User request. Return the transition
alone on that branch; the non-resume path is unchanged.
Fixes #6239
Co-authored-by: John Doe <johndoe@example.com>
* fix(daemon): retry Azure alias token parameter (#6718)
* test(ci): harden Functional E2E and daemon release gates (#6708)
* test(e2e): harden coverage and runtime isolation
* test(e2e): gate prerelease on full functional suite
* test(daemon): enforce prerelease coverage
* test(daemon): stabilize full-suite fixtures
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* test(e2e): preserve interaction regression coverage
Keep deck navigation subject to Playwright actionability checks and restore the current rail updater/composer stacking witness as an explicit expected failure.
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(ci): close conformance validation gaps
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* test(e2e): restore MCP snippet style guard
Restore the browser-computed style witness that was over-deleted when the OAuth flow moved to Integrations, and navigate through the current Settings MCP server entry.\n\nGenerated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(ci): close directive and job-boundary gaps
Compare normalized MUST_FIX directives as sets so identical multi-item rounds remain weak debates, and bound workflow topology assertions to their immediate sibling jobs.\n\nGenerated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(web): load custom skill files after create (#6735)
* fix(daemon): inline shared pipeline atom bodies once across active-stage blocks (#6245)
The system-prompt composer rendered each pipeline stage's atoms
independently, so an atom declared by multiple stages (od-default
declares discovery-question-form in both task-type and discovery) had
its full SKILL.md body inlined once per stage β duplicating ~70 lines
in every default free-form Home run's system prompt.
Add renderActiveStageBlocks to @open-design/contracts: it renders all
stage blocks while inlining each atom body exactly once; a later stage
that re-declares an atom keeps its subsection header but carries a
one-line back-reference to the stage that inlined the body. Stage
headers stay per spec Β§23.4. The server's activeStageBlocks build now
goes through this builder, so any plugin sharing atoms across stages is
handled at composition time.
Fixes #6238
Co-authored-by: John Doe <johndoe@example.com>
* fix Codebuddy model discovery from CLI help (#6617)
Co-authored-by: zzjjzz-zz <210207242+zzjjzz-zz@users.noreply.github.com>
* refactor(media): remove Codex image generation (#6742)
* refactor(media): remove Codex image generation
* fix(media): preserve HyperFrames video default
* test(media): keep Vela coverage image-only
* fix(media): persist Home image model selection
* docs(i18n): translate Korean browser assist UI (#6212)
Co-authored-by: Ray Xi <2667192167@qq.com>
* fix(i18n): refresh French locale fallbacks (#6612)
* fix(i18n): refresh French locale fallbacks
* fix(i18n): address French review notes
* ci: gate configured contributors on maintainer check (#6747)
* docs(release): add 0.19.0 release notes (#6758)
* fix social share icons in packaged app (#6750)
* fix(daemon): report missing vela runtime as unavailable (#6738)
* fix(daemon): report missing vela runtime as unavailable
* fix(web): keep message center public without amr runtime
* fix(daemon): cache workspace authority reads safely (#6752)
* fix(daemon): cache workspace authority reads safely
* chore(daemon): expose workspace authority metrics
* fix(daemon): enable adaptive workspace authority by default
* fix(web): refresh workspace snapshot on SSE reconnect
* fix(daemon): recover adaptive workspace authority
* fix(web): hide stale run recovery cards after success (#6748)
* fix(web): hide stale run recovery cards
* fix(web): scope recovery errors to assistant runs
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(web): prefer current pane errors in recovery card
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(web): prevent duplicate queued chat submissions (#6749)
* fix(web): scope artifact upgrade gate to AMR (#6760)
* fix(daemon): keep daemon as sole writer of run events on stale message PUT (#6418)
* fix(daemon): keep daemon as sole writer of run events on stale message PUT
The web client saves whole-message snapshots through PUT /messages/:mid.
A stale snapshot β captured in memory before a reconnect or project
switch, then PUT after the daemon appended more events β overwrote
`events_json` and wiped early run events, including the `status:model`
event the conversation page renders its Model module from (#6396).
Make the daemon the single writer of the daemon-owned fields for
daemon-backed assistant messages (stored role=assistant with a run_id):
the PUT route now keeps the stored events / content / last-run-event id /
run status instead of the client snapshot's values. Client-owned metadata
(feedback, comment attachments, telemetry) still flows through the same
PUT.
Adds getMessage (single-message getter) and a concurrency regression
test: a real run persists its early status event, then a stale snapshot
PUT must not wipe it β red before the fix, green after.
* fix(daemon): scope stale-snapshot guard to event-list regression
The #6396 guard previously preserved stored events/content/run-status for
every daemon-backed assistant message, which broke mock-agent flows (UI P0
app-restoration): e2e suites mock the run SSE end-to-end, so the daemon
never persists events and the web client is the legitimate writer of the
assistant message's content/run-status.
Run events are append-only, so a stale snapshot can only shrink the stored
list. Narrow the guard to a no-regression rule: preserve the stored
events/content/last-run-event-id/run-status only when the incoming
snapshot would drop already-persisted events. A web write that carries at
least as many events still flows through, keeping mock-agent flows and
client metadata writes working.
Adds a regression test for the mock-agent path (web writes events and a
terminal runStatus when the daemon never persisted any).
* fix(daemon): preserve daemon ownership marker across repeated stale PUTs
The stale-snapshot guard preserved events/content/last-run-event-id/run
status but spread the incoming snapshot's runId/role unchanged. A snapshot
captured before `/api/runs` assigned a run id omits `runId`, so
upsertMessage wrote `run_id = NULL` β dropping the message back out of the
daemon-backed path on the NEXT stale PUT, which could then wipe the
protected events/content again (looper review on #6418).
Preserve the stored role + runId alongside the other daemon-owned fields
when the guard fires, and extend the regression test to replay two stale
PUTs that both omit runId, asserting the run id, events, content, and
terminal status survive both.
* fix(daemon): latch terminal run status against equal-length stale snapshots
The length-based freshness check only caught snapshots that SHRANK the
stored event list. But the daemon writes the terminal run_status in a
separate UPDATE (no event appended), so a web snapshot captured after the
final event but before that write has the SAME event count while still
carrying a non-terminal status β it previously flowed through and regressed
the stored status back to 'running', sticking conversations in a
nonterminal state (looper review on #6418).
Make terminal run status a daemon-owned latch: once the stored status is
terminal (succeeded/failed/canceled), a client PUT that carries any other
status is rejected for the daemon-owned fields. Combined with the existing
event-shrink rule, and preserving role/runId on both paths.
Adds a regression test: run completes (terminal status + events), then an
equal-length snapshot with 'running' is replayed and must not regress the
terminal status or ownership fields.
* fix(daemon): preserve run lifecycle timestamps across stale message PUTs
The stale-snapshot guard protected events/content/run-status/ownership but
a snapshot that omits startedAt/endedAt still nulled the daemon-written
run timestamps via upsertMessage β and saved.endedAt feeds persisted-run
telemetry, so a terminal message lost its timing data (looper review on
#6418).
Preserve stored startedAt/endedAt alongside the other daemon-owned fields
in the stale-write branch, and assert in the regression test that the
timestamps survive repeated stale PUTs.
* fix(daemon): let metadata updates write fresh timestamps on stale-snapshot path
Preserving startedAt/endedAt unconditionally in the stale-snapshot branch
broke the retry flow: a metadata update that carries a NEW endedAt (empty
events, matching terminal status) was treated as a stale full snapshot, so
the web's fresh completion timestamp was discarded and the daemon's was
kept (e2e retry-after-stop).
Preserve the lifecycle timestamps only when the snapshot OMITS them (the
#6396 stale case, where upsertMessage would null started_at/ended_at) and
let a snapshot that genuinely provides them write through. Events, content,
terminal status, and ownership fields keep the no-regression protection.
Adds a regression test for the metadata-update path: a fresh endedAt lands
while daemon-owned events and terminal status survive.
* fix(daemon): scope stale-snapshot lookup to the authorized conversation
Two data-integrity gaps from the #6418 guard (looper review):
1. getMessage selected globally by id, so a message id from another
conversation could be read (data disclosure in the response) and
rewritten through the wrong project/conversation endpoint. Scope the
lookup by conversation_id and reject a PUT whose message id belongs to
a different conversation.
2. The timestamp exception let a stale snapshot carrying an older endedAt
overwrite the daemon's newer value. Make lifecycle timestamps monotonic
watermarks: startedAt keeps the earliest (the daemon's first start),
endedAt only advances β while a metadata update that genuinely advances
endedAt (retry flow) still lands.
Adds regression tests: a cross-conversation message id is rejected and the
other conversation's row is untouched; monotonic timestamps preserved.
* fix(daemon): never regress daemon-written startedAt on stale message PUTs
The stale-write branch accepted an earlier startedAt from the incoming
snapshot ("earliest wins"), so a captured snapshot carrying an older start
time inflated persisted duration/telemetry even though events/content/
status were protected (looper review on #6418).
startedAt is the daemon's first start (COALESCE keeps it), so keep the
stored value unconditionally in the stale branch; endedAt stays an
advance-only watermark so the retry flow's fresh completion timestamp still
lands. Extend the stale-snapshot regression with an explicitly older
startedAt that must not replace the stored value.
* fix(daemon): keep pinned runId on stale pre-run message PUTs
A daemon-backed row pinned by /api/runs (runId set, runStatus queued, no
events yet) followed by a stale web snapshot that omits runId used to have
its run_id nulled: neither shrinksEvents nor regressesTerminalStatus fires
on an empty event list with a non-terminal status, so the early return
spread the runId-less snapshot unchanged. The row then dropped out of the
protected path and the next stale PUT could wipe events once they landed.
The no-regression early return now preserves the daemon-ownership markers
(role + runId) while still letting the client's non-terminal status and
metadata flow. Regression test drives the real PUT route (#6418 review).
* fix(daemon): let same-message retries with a new runId replace the failed attempt
The stale-write guard added for #6418 correctly preserved role+runId on the
no-regression early return, but it also caught legitimate same-message
retries: side-chat retries reuse the failed assistant's id, and
pinAssistantMessageOnRunCreate assigns a NEW run id while keeping the old
terminal status/events/content until the retry's final PUT. The guard then
routed that snapshot into the stale branch, keeping the retry stuck on the
previous attempt's failure.
A snapshot carrying a different runId is the retry (not a stale copy of the
old run), so it now flows through untouched. Regression test drives the real
PUT route (nettee review on #6418).
* fix(daemon): reset run generation at pin time so retries replace the failed attempt
The same-message retry path stays stuck on the previous failure: after
pinAssistantMessageOnRunCreate rebinds a terminal assistant row to a new run
id, the retry's final PUT shares that runId, so the no-regression guard
classifies it as a stale shrink/status regression and restores the old
failure. A runId carve-out in the guard cannot fix this (the ids already
match post-pin) and would open a stale-write hole for old-run snapshots.
Make pin the generation boundary instead: when a run creation rebinds an
existing assistant message, reset the run-owned fields to this run's start β
run_status to the (non-terminal) run status, events/content cleared, ended_at
nulled, started_at set to the run's createdAt. The retry's final PUT then
flows through the guard normally. Also preserve pin-written startedAt on the
eventless early-return path.
Tests model the post-pin shape (same runId, fewer events, fresh terminal
status) and assert the pin-written start time survives a stale pre-run
snapshot (mrcfps review on #6418).
* fix(daemon): scope run pin to the conversation and clear last_run_event_id on rebind
Addresses the four findings on #6418 (mrcfps + nettee):
- pinAssistantMessageOnRunCreate now scopes both the SELECT and the UPDATE to
the run's own conversation (and skips a no-row pin whose message id belongs
to another conversation), so a mis-scoped run can no longer wipe another
conversation's assistant message (nettee P1).
- A rebind to a NEW run id now also clears last_run_event_id, so a reattach
never resumes from a superseded run's event cursor (mrcfps P1).
- A same-run re-pin (AMR recharge-resume) only refreshes status/context and
keeps the persisted transcript (mrcfps P2).
- mergeMessageWriteForDaemonBacked discards a delayed PUT from a superseded
run generation (incoming.runId differs from stored.runId), keeping the
current generation's run fields while letting metadata land (nettee P2).
Tests: new pin unit suite (rebind reset / resume keep / cross-conversation
skip) and a stale-message e2e case for the old-generation discard.
* fix(daemon): enforce assistant-message ownership and resume lifecycle timestamps
Addresses the three data-integrity blockers on #6418 (nettee 8/8):
- Ownership end-to-end: POST /api/runs and /api/chat now reject an
assistantMessageId that references a user message (409
INVALID_ASSISTANT_MESSAGE) or an assistant message in another conversation
(409 IDEMPOTENCY_CONFLICT), and pinAssistantMessageOnRunCreate scopes its
lookup by role too - a mis-scoped run can no longer pin/append/finalize a
row it does not own.
- Recharge resume: the same-run pin branch clears ended_at so the resumed
completion records a fresh terminal timestamp instead of the prior
failure's end.
- Stale terminal snapshot: the PUT guard discards a terminal failed snapshot
when the stored row is non-terminal (i.e. after a same-run resume), so the
resumed run cannot relatch the old failure; the current run's failure is
finalized by the daemon, never by a web PUT.
Tests: route ownership rejections (foreign conversation + user row), pin
unit ended_at clear, and a stale-message e2e case for the resume relatch.
* fix(daemon): reject unverifiable assistantMessageId and active-run rebinds
Addresses the two additional ownership/lifecycle hazards on #6418 (nettee 8/8):
- POST /api/runs and /api/chat now reject a supplied assistantMessageId when
no conversation can be resolved (400) β without a conversation there is
nothing to validate ownership against, and the run would mutate a foreign
row via the id-only writers.
- Both routes also reject an assistantMessageId whose existing row is owned
by a still-active (queued/running) run (409 RUN_IN_PROGRESS), so two
concurrent runs sharing the id cannot reset each other's in-flight
generation. pinAssistantMessageOnRunCreate adds the same skip as defense in
depth.
Tests: route regressions for no-conversation (400) and active-run rebind
(409), plus a pin unit case asserting an active row's transcript is never
generation-reset.
* fix(daemon): allow runId-less web placeholders through the active-run guard
The active-run rebind guard broke the normal pre-run flow: the web persists an
assistant placeholder with runStatus set but no run bound yet, then submits
the run with that assistantMessageId. The guard read runStatus only and
rejected it as RUN_IN_PROGRESS, failing the e2e dialog suites.
Scope the guard to rows bound to a REAL run (run_id non-empty): only those can
be owned by a still-active concurrent run. A runId-less placeholder stays
rebindable. Same condition applied to the pin-side defense-in-depth skip.
Tests: pin unit case for a runId-less placeholder binding to the new run;
existing active-run rebind rejection (with a real runId) unchanged.
* fix(daemon): keep web-persisted placeholder startedAt on first pin
The generation-reset rebind overwrote started_at with the run's createdAt for
a runId-less web placeholder, clobbering the turn's own start time the UI had
already persisted (retry-after-stop e2e asserted the stopped attempt keeps its
startedAt).
Only a retry that rebinds an OLD-run row resets started_at to the new run's
createdAt; a runId-less placeholder keeps its web-persisted start via
COALESCE. Pin unit test updated to assert the placeholder start time survives.
* fix(daemon): make rebind concurrency guard daemon-aware; drop staleResumeFailure
The route-level active-run guard was rejecting the normal UI flow: the web
persists a runId-less assistant placeholder, submits the run, and after a
failure clicks retry β the guard read the message row's own runStatus and
rejected both as RUN_IN_PROGRESS, failing the dialog/workspace-restoration
e2e suites. The message row's runStatus is not authoritative for concurrency.
- The active-run reject now consults design.runs.get(runId): only when the
daemon STILL has that run active is the rebind rejected (409
RUN_IN_PROGRESS). A runId-less placeholder, or a run the daemon has already
finished (normal retry), stays rebindable.
- Drop the staleResumeFailure heuristic: it treated every non-terminal stored
row + incoming failed as a stale post-resume snapshot, which also swallowed
a genuine run failure (stored queued -> incoming failed) and left the
message stuck non-terminal, breaking the retry chain. Genuine failures now
persist again; post-resume stale-snapshot relatch is left to a future
daemon-side generation boundary rather than an unreliable heuristic.
Tests: retry/placeholder rebind now asserts the daemon-no-longer-owns case
succeeds (no RUN_IN_PROGRESS); the staleResumeFailure-only test is removed.
* fix(daemon): exempt idempotent retries and confirm failures via daemon state
Addresses the nettee re-review on b4c33bf:
- The active-run rebind guard now exempts a same-clientRequestId idempotent
retry: when the daemon run's clientRequestId matches the request, the daemon
reuses the original run and must not be rejected as RUN_IN_PROGRESS
(run-request-idempotency suite).
- The post-resume stale-snapshot guard is now daemon-aware: a terminal failed
snapshot against a non-terminal stored row is discarded only when the daemon
does NOT report the run as failed (stale pre-resume copy). A failure the
daemon confirms (written via reconcileAssistantMessageOnRunEnd) still lands,
so genuine run failures persist and the retry chain works.
run-request-idempotency (6) + stale-message + pin + cross-project (20) pass;
daemon typecheck clean.
* fix(daemon): make assistant-message ownership atomic at run creation
The core concurrency hole (PerishCode): two runs sharing an assistantMessageId
could both create and write through the same message row, because the pin was
a void best-effort β on detecting an active owner it skipped, and the run
started anyway. Every route-level heuristic to close it mis-fired on the
normal UI flow (placeholder, retry, idempotent retry).
Make ownership atomic: pinAssistantMessageOnRunCreate is now an atomic claim
inside db.transaction().immediate(), executing a single conditional UPDATE
(WHERE run_id IS NULL OR run_id = <this run> OR run_status IN
(succeeded,failed,canceled)) that returns changes>0. A claim failure drops
the just-created run (design.runs.drop) and rejects with 409 RUN_IN_PROGRESS,
so no run ever starts without owning its assistant message. The racy
design.runs.get pre-checks are removed; placeholder/retry/idempotent semantics
fall out of the claim's WHERE conditions (same-run refresh, terminal rebind
generation reset, placeholder startedAt preservation).
Tests: run-atomic-ownership.test.ts β two concurrent POSTs with the same
assistantMessageId resolve to exactly [202, 409], only the winner spawns a
child, and a web-persisted placeholder startedAt survives the first claim.
Existing pin/stale-message/cross-project/idempotency suites (69 total) pass.
* fix(daemon): arbitrate terminal writes, claim before seed, never drop resumed runs
Addresses nettee's 8/10 review on the atomic-ownership head:
- Terminal-write arbitration (conversations.ts): the guard now keys on ALL
client terminal statuses against what the daemon positively knows. A daemon
it does not know (mock/client-owned row) accepts the client terminal write;
a daemon-owned run still active preserves every daemon field (no premature
succeeded/failed/canceled latch, no reopen for a competing claim); a daemon
terminal that disagrees is authoritative. This stops both discarded
mock-path failures and premature-terminal rebinds.
- Claim before seed (runs.ts POST /api/runs): the atomic ownership claim runs
BEFORE user-message seeding, so a rejected concurrent run never leaves an
orphan user turn.
- Resume safety (runs.ts + chat-run-messages.ts): a resumed run's claim runs
before prepareRestart and only freshly-created losers are dropped β a
resumable run that loses a claim stays terminal + resumable instead of being
deleted. pinAssistantMessageOnRunCreate gains a status override so a resume
claim writes the post-restart queued intent.
Tests: status-override pin unit; concurrent orphan-user-turn route case.
71 tests + typecheck pass.
* fix(daemon): key terminal arbitration on daemon state, not stored status presence
nettee 8/10 blocking: a stale whole-message PUT that omits runStatus used to
null the daemon-owned status column, which reopened the terminal gate β a
later premature terminal PUT could then latch succeeded/failed onto an active
daemon run and make the row re-claimable.
- Terminal arbitration now keys on the daemon lookup for ANY non-terminal (or
status-less) stored row, not only when stored.runStatus is defined.
- The accepted (no-regression) merge preserves the stored run status when the
snapshot omits it, and applies the same monotonic endedAt watermark.
- New integration regression: status-less PUT -> premature terminal PUT against
an active run -> competing claim is still rejected with 409.
72 tests + typecheck pass.
* fix(daemon): return ok from the workspace-gate claim stub
The atomic claim contract changed from void to { ok, reason }; the
run-create-workspace-gate suite's injected stub still returned void, so every
created run was treated as an unclaimed loser (31 failures). Return { ok: true }
from the stub β the suite is back to its 3 pre-existing BYOK failures.
* fix(daemon): preserve role on the terminal-arbitration preserve path
The arbitration preserve return was missing role: stored.role (sibling preserve
paths already restore it), which could let a stale snapshot carry a different
role. Restore it for consistency with the daemon-ownership marker.
* fix(daemon): preserve run seed message order
* fix(daemon): reject aliased run message ids
* fix(daemon): preserve stale write event cursor
* fix(daemon): preserve placeholder content on run claim
* fix(daemon): validate chat assistant conversation claims
* fix(daemon): harden run message ownership edge cases
* fix(daemon): preserve content on equal-length stale snapshots
* fix(daemon): protect partial equal-length content snapshots
* fix(daemon): preserve daemon content on event growth
* fix(daemon): atomically seed claimed run user turn
* [verified] fix(pack): use runtime path for portable NSIS logs (#6559)
* docs(spec): update collab realtime current-state summary (#6593)
* fix(deps): patch Docker runtime CVE floors (#6733)
* fix(landing-page): stop the Launch Week hero sliding under the header (#6766)
On a short phone the wordmark was cut in half by the fixed site header β
reported from the release channel with a screenshot of the Chinese page.
`.hero-main` centres its column, and a centred flex container overflows in
*both* directions: once the hero content is taller than its box, half the
excess goes up, and up is where the header is. It only reproduces when the
viewport is short β 360x640 and 375x667 fail, 390x844 passes β which is why
the earlier sweep at 390 missed it entirely.
Two changes. `justify-content: safe center` keeps the centring while there is
room and falls back to start-alignment the moment there isn't, with a plain
`center` beneath it for engines that don't parse the keyword. And on phones the
hero's fixed `height` becomes `min-height`, so content that doesn't fit grows
the section instead of being clipped by it.
Verified across 11 locales x 320/360/375/390/768/1440, asserting the wordmark's
top edge clears the header's bottom edge β the check the earlier sweep lacked,
since it only looked for horizontal overflow and missing elements.
Co-authored-by: Scarlett Huang <scarletthuang@Scarletts-MacBook-Air.local>
* fix(web): enter project immediately after Home send (#6741)
* fix(web): enter project immediately after Home send
* fix(web): close optimistic project failure paths
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(web): describe mcp slash commands (#6597)
* fix daemon auth for Docker browser peers (#6715)
* fix daemon auth for Docker browser peers
* fix: restore route API token predicate
* test: send explicit Docker browser host headers
* docs: fix localized Docker browser URLs
* fix(daemon): let the workspace_resources envelope decide design-system read visibility (#6776)
A brand re-finalize drops the workspaceId claim from a personal design
system's metadata.json while the workspace_resources envelope keeps the
binding correct. The read side trusted the metadata claim alone, so a
correctly bound system vanished from the scoped catalog and every by-id
read (catalog, detail, preview, showcase, static).
List the user directory unscoped and make the exact envelope binding
(workspaceId + personal + active + createdByWorkspaceMemberId) the
visibility authority for scoped requests; the signed-out lane now hides
both envelope-bound and metadata-claimed systems. By-id reads follow the
same authority, and the detail/preview/showcase/static routes forward the
verified workspaceMemberId they already resolve for the catalog.
Fixes #6763
* fix(cli): make `od project list` honor the signed-in workspace (#6679) (#6736)
* fix(cli): make od project list honor the signed-in workspace (#6679)
od project list always called GET /api/projects (the NO-SCOPE catalog),
which after 0.18.0's workspace isolation returns only projects never
adopted into a workspace. immediately
workspace-adopts its result, so reported 0 projects
while the UI listed them (#6679).
Mirror #6595's MCP fix: when no explicit --workspace/--workspace-member
pair is supplied, resolve the signed-in workspace once via
GET /api/workspace/directory (caching the result for 15s to avoid
repeated directory hits during interactive use), then route to
GET /api/workspaces/:id/projects with the workspace headers. When the
directory is unavailable (signed-out, non-vela, or transient outage),
fall back to the original headerless catalog so unbound projects still
appear. An explicit --workspace pair wins and short-circuits the
auto-resolution, preserving the existing behavior.
Refs: #6569, #6595 (MCP bridge fix, cli.ts line 6589 in 0.16.2)
Refs: #6679 (CLI/UI project list inconsistency)
Signed-off-by: xxiaoxiong <2482929840@qq.com>
* fix(cli): route explicit --workspace to the scoped catalog too (#6679)
PerishCode review caught that the implicit signed-in path now uses the
scoped catalog, but an explicit --workspace/--workspace-member pair still
fell back to /api/projects, which is the unbound catalog β passing
--workspace to /api/projects does NOT scope it (#6679 repro). Route both
the implicit resolved identity and the explicit flags through
GET /api/workspaces/:id/projects so the explicit path fixes the same
#6679 behavior the implicit path fixed.
Updates the explicit test case to assert the workspace-scoped URL, and
adds a /api/workspaces/ws-1/projects branch to the test stub server.
Signed-off-by: xxiaoxiong <2482929840@qq.com>
---------
Signed-off-by: xxiaoxiong <2482929840@qq.com>
* Fix workspace-scoped MCP resources/list and resources/read (#6770) (#6773)
* Fix workspace-scoped MCP resources/read and resources/list (#6770)
The MCP `resources/list` and `resources/read` handlers were calling
`/api/skills` and `/api/design-systems` without workspace headers, while
the project/run tools (after #6569) all resolve the signed-in workspace
and forward `x-od-workspace-id` / `x-od-workspace-member-id`.
This meant an MCP client listing resources only saw the NO-SCOPE catalog:
any Personal design system that the workspace actually owned was filtered
out, and reading `od://design-systems/<personal-id>/DESIGN.md` returned
`404 design system not found` from the daemon even though the binding
exists. The same blind spot affected `/api/skills` listing.
Fix: route both handlers through the same `resolveMcpWorkspaceContext`
that the project/run tools use, and forward the headers on the listing
and read calls. The two handlers are extracted to exported
`_listMcpResources` / `_readMcpResource` helpers so the logic is
reachable from a vitest harness without spinning up a real stdio MCP
server; the original `server.setRequestHandler` bodies now delegate to
those helpers (no behavioral change to the stdio path).
Tests: `apps/daemon/tests/mcp-resources-workspace-scope.test.ts` covers
the header forwarding on both `resources/list` and `resources/read`, the
NO-SCOPE fallback when no workspace resolves, the brief-app resource
fast path that bypasses the daemon, and the unsupported-URI rejection.
* Export OPEN_DESIGN_BRIEF_APP_RESOURCE for tests
The new mcp-resources-workspace-scope test imports this constant to
assert the brief-app resource fast path bypasses the daemon. It is
referenced inside mcp.ts but never exported β typecheck broke with
TS2459.
* fix(packaged): widen macOS sidecar status timeout to 90s for 0.18.1 cold starts (#6762)
Packaged 0.18.1 on Apple Silicon can exceed the 35s daemon/web status
budget during a cold launch, causing startPackagedSidecars() to time
out, tear the sidecars down, and leave the desktop looping on a stale
currentWebUrl / od:// 502. Give darwin the same 90s "slow, not dead"
safety net already used on win32 and linux for similar cold-start
failure classes (issue #6637).
Generated with [Devin](https://devin.ai)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(design-systems): add Cloudflare Kumo UI (#6310) (#6769)
* fix: update explicitly named chat artifacts in place (#6688)
* fix: prevent chat artifact overwrite on empty API response
* fix(web): keep pointer resolution intact for explicitly identified artifacts
The previous head skipped the collision loop entirely when an artifact carried
an explicit identifier, which left fileName equal to the existing target.
resolveHtmlPointerArtifactTarget returns null when the pointer target equals
its candidate, so a pointer artifact naming an existing page fell through to
validateHtmlArtifact and was refused.
Probe collisions into a separate name and pass that to the pointer resolver,
while still overwriting the identified file. tests/components/ProjectView.api-empty-response.test.tsx
passes (20/20).
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* fix(web): keep recovered successful runs marked done (#6764)
* perf: bound large project background work (#6788)
* fix(ci): pin Blacksmith apt mirror before Playwright deps install (#6790)
* fix(ci): pin Blacksmith apt mirror before Playwright deps install
* ci: retrigger on blacksmith runner mode
* feat(mcp): add claude-desktop to MCP install targets (#6489)
* feat(mcp): add claude-desktop to MCP install targets
Add 'claude-desktop' agent slug with JSON merge strategy.
Writes to the platform-appropriate config path:
- macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
- Windows: %APPDATA%/Claude/claude_desktop_config.json
- Linux: ~/.config/Claude/claude_desktop_config.json
Claude Desktop has native MCP support and reads from
claude_desktop_config.json using the standard {mcpServers: {...}} schema.
Also updates README table and file header comment.
* fix(mcp): refuse claude-desktop on unsupported platforms
Claude Desktop is only available on macOS and Windows.
The planAgentInstall case now returns a ManualInstallPlan with
a clear reason on other platforms, instead of silently writing
to an invented ~/.config/Claude/ path.
Changes:
- planAgentInstall: branch on platform, return manual plan for
non-darwin/non-win32
- claudeDesktopConfigPath: remove Linux fallback path
- Tests: flip Linux test from json-write expectation to manual-plan
with unsupported-platform assertion
* docs: clarify Claude Desktop MCP config is macOS/Windows only
Update the unsupported-platform reason, test, and README footnote to say
automatic MCP configuration (not Claude Desktop itself) is currently
supported only on macOS and Windows. Claude Desktop is now available on
Linux (beta), but the authoritative local-MCP config path is not yet
documented for that platform.
Ref: https://support.claude.com/en/articles/10065433-install-claude-desktop
* perf(collab): batch team resource pulls (#6782)
* perf(collab): batch team resource pulls
* fix(nix): refresh pnpm dependency hashes
* fix(nix): refresh merged pnpm dependency hashes
* test(e2e): cover Vela batch pull compatibility
* fix(release): harden prerelease E2E validation (#6755)
* fix(release): harden prerelease E2E validation
* fix(daemon): refresh active run tool tokens
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* chore(release): keep Feishu notification unchanged
* fix(daemon): retain media tasks for active runs
Associate token-authored media tasks with their owning run and defer terminal cleanup while that run still has an active tool token. Correct the prerelease status document to reflect the final Feishu notification topology.
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* test(e2e): defer system theme coverage
* test(e2e): remove retired design system settings flows
* fix(e2e): close prerelease coverage gaps
* fix(release): remove unrelated UI changes
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(e2e): restore remaining prerelease coverage
* docs(e2e): record remaining scoped gaps
* test(e2e): isolate workflow command fixtures
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* test(e2e): preserve Node in workflow fixture PATH
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* fix(ci): skip Playwright apt deps on Blacksmith runners
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
* Revert "fix(ci): skip Playwright apt deps on Blacksmith runners"
This reverts commit 201f3f3ee3916d10eeb517cc02dae86e988c3609.
* fix(plan): reliably open generated artifacts
* fix(plan): reject unconfirmed artifact paths
* fix(amr): recover expired cloud sessions (#6786)
* fix(amr): recover expired cloud sessions
* fix(diagnostics): redact dynamic API routes
* fix(daemon): honor settings in AMR diagnostics
* fix(daemon): align workspace auth revisions
* fix(web): gate expired cloud sessions on sign-in
* fix(web): scope cloud sign-in gate to cloud execution
* test(web): align cloud auth fixture
* fix(web): recover transient run authority failures
* test(daemon): expect retryable authority detail
* test(e2e): align BYOK auth boundary
* test(e2e): align expired cloud recovery flows
* fix(web): prevent mobile comment preview flash (#6785)
* fix(release): gate prerelease on P0 and skip current blockers (#6808)
* fix(release): gate prerelease UI on P0
* test(release): skip current prerelease blockers
* test(e2e): make beta recovery fixture version-independent (#6817)
* test(e2e): make beta recovery fixture version-independent
* test(daemon): await restart analytics recovery
---------
Co-authored-by: Amy <1184569493@qq.com>
* fix(web): streamline Home submit preflight (#6756)
* fix(web): retry transient home balance preflight
* refactor(web): streamline home submit preflight
* fix(web): keep local submit independent from cloud identity
* fix(web): preserve local and legacy submit lanes
* fix(web): keep in-memory workspace witness valid
* fix(web): retain workspace preflight compatibility
* fix(web): require workspace witness for team plugin apply
* fix(plugins): apply exact local plugin source
* fix(web): invalidate legacy witness on identity change
* refactor: keep Home submit local-first
* test: align local-first creation coverage
* fix: preserve exact local plugin context
* fix: fence exact local plugin use
* fix: disable inexact legacy plugin fallback
* test: mock exact plugin apply in visuals
* fix: reject unsupported exact project source
* Revert "fix: reject unsupported exact project source"
This reverts commit 99e0cf23c1576fb73eca1490d3cea3ca4a09b427.
* Revert "test: mock exact plugin apply in visuals"
This reverts commit 33b36688c7a634b2248ae91e47404b0ac30efb87.
* Revert "fix: disable inexact legacy plugin fallback"
This reverts commit d1b4d9bcaa965ea3dcea61f8d6b857b37a49ece5.
* Revert "fix: fence exact local plugin use"
This reverts commit aebaf6a20bdf0b99877c596e89aec92c04f470cf.
* Revert "fix: preserve exact local plugin context"
This reverts commit aa6ab676312d0750847c97516c9c360e46984f76.
* Revert "fix(plugins): apply exact local plugin source"
This reverts commit e776a7d0799b238411110ec6b7561c0c87004f32.
* fix: reconcile local submit selections
* fix: preserve cold cloud submit handoff
* test: seed active team design-system binding
* fix(plugins): preserve exact local source semantics
* test(web): align Home failure copy after main merge
* fix(home): preserve staged resource catalog scope
* Updating sidecars to accept the allowed internal host env variable (#6802)
* fix(web): gate comment bridge readiness on the committed preview URL (#6797)
The od:url-selection-bridge-ready latch now requires the message to carry the document href and match the current iframe src, so a stale ready from a previously loaded document can no longer keep Comment on a bridge-less URL-load preview. The daemon bridge posts href in both the probe reply and the initial ready. Falls back to srcDoc comments when no matching ready arrives.
Fixes #6702
* fix web powered preview behind reverse proxies (#5592)
* test(e2e): assert Cloud sign-in after logout (#6828)
* fix(web): name the reason when an invite target is already a member (#6830)
Inviting an address that is already an ACTIVE member of the workspace
showed the generic "invitation failed, try again later" copy, so the user
had no way to learn that retrying could never succeed.
The dialog already branches on B's allowlisted invite-create codes; the
gap was in the allowlist itself. B rejects that case with HTTP 409
`{"error":"invite_existing_member"}`, which was absent from
WORKSPACE_INVITE_CREATE_ERROR_ALIASES, so it degraded to the
transport-shaped `create_409` and fell through to the retry-later
fallback. Add the alias, and split the two 409 duplicate variants apart:
"already a team member" and "already has a pending invitation" call for
different next steps and no longer share one merged sentence.
The code -> copy mapping moves out of the component into
`collab/invite-error-copy.ts` as a pure code -> i18n-key function, so a
new B code has exactly one place to be taught and the mapping is
testable on its own.
Verified red on the alias-less baseline (4 failures across both specs)
and green after.
Record: recvrovm9Bcyy0 (V0.19.1 acceptance)
* fix(daemon): preserve directory-verified workspace authority (#6813)
* fix(daemon): preserve verified workspace authority
* fix(daemon): verify authority before cached enrichment
Generated-By: looper 0.11.8 (runner=fixer, agent=codex)
---------
Co-authored-by: Amy <1184569493@qq.com>
* fix(landing): sync Team seat pricing with the $5 seat-fee catalog (#6834)
Vela #1263 dropped the Team seat fee to $5 and re-derived every Team
tier off it, but /pricing kept quoting the old catalog β Team Pro read
$120 / seat / month against a real $105, and the three-seat first-year
total read $2,592 against a real $2,268. Anyone comparing the marketing
page with the in-product upgrade modal saw two different prices.
The seat prices are percentage-derived off the cloud catalog and no
longer land on whole dollars ($73.50 first month, $100.63 on the yearly
Max coupon), so `formatUsd` and its browser-side twin now render cents
when an amount actually has them and keep whole dollars otherwise. The
individual Creator tiers are untouched and still print as whole dollars.
Prices come from db/seeds/local/seed-team-billing-catalog.py:
tier list /seat/mo 1st month yearly /seat/mo 1st year
Team Basic $5 $4 (-20%) $3.50 (-30%) $42
Team Plus $25 $20 (-20%) $17.50 (-30%) $210
Team Pro $105 $73.50(-30%) $63 (-40%) $756
Team Max $205 $123 (-40%) $100.63 (-51%) $1,207.61
pricing.md and the plans.json contract move with the snapshot, and the
contract test's golden totals are re-derived from the new catalog.
Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com>
* 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.
* test(e2e): cover shared design system logo assets (#6825)
* test(e2e): cover shared design system logo assets
* test(e2e): reuse team design system collaboration cluster
* test(e2e): select team workspace before owner logo check
* test(e2e): activate owner team workspace before logo checks
* test(e2e): wait through workspace tab restoration
* feat(design-systems): ship structured runtime workflow (#6805)
* feat(design-systems): add structured runtime contract (#6456)
* feat(design-systems): add structured runtime contract
* chore: retrigger CI for PR 6456
---------
Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: mrcfps <mrc@powerformer.com>
* feat(design-systems): resolve structured runtime intents
* fix(design-systems): preserve workspace runtime scope
* test(design-systems): cover scoped prompt runtime
* fix(design-systems): use one component authority
* fix(design-systems): preserve run workspace authority
* fix(design-systems): honor runtime rollback gate
* fix(design-systems): pin run binding authority
* fix(runs): reject client-supplied scopes
* fix(design-systems): keep user package assets scoped
* feat(design-systems): validate generated UI adherence
* test(e2e): assert durable reload authority state
* fix(design-systems): reject non-executable adherence evidence
* fix: harden design system adherence validation
* fix(design-systems): validate JSX selectors structurally
* fix(design-systems): close adherence input gaps
* chore(nix): refresh pnpm dependency hashes
* chore(ci): retrigger design system validation
* fix(daemon): bound design system artifact reads
* fix(design-systems): bridge Codex runtime tools
* docs(codex): clarify shell environment policy
---------
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: mrcfps <mrc@powerformer.com>
* test(daemon): restore prerelease blocker coverage (#6812)
* fix(web): scope DeepSeek campaign badge to AMR (#6841)
* fix(web): include personal projects in search (#6838)
Co-authored-by: ι»ζ‘ <huangtao@huangtaodeMBP.lan>
* feat(web): refresh home and preview workspace controls (#6692)
* feat(web): home type-fan picker, floating account cluster, docked tab dropdown
Home hero: the 12 create-scenario types render as a fanned card carousel
above the composer (flat elliptical arc, apex card = selected, arrows /
card click switch along the arc; selected card scales about its own
center on an inner body so the outer transform stays a pure arc move).
Entry chrome: the account module leaves the nav-rail bottom and rides a
fixed top-right cluster next to the DeepSeek campaign badge β credits as
its own pill, avatar-only circular trigger, hover menu opens downward
(right-aligned, width-capped). The cluster is app-region no-drag so the
menu stays reachable in the desktop shell.
Project route: the docked workspace tab strip becomes one white dropdown
spanning the chat column (MRU order, most recent first), and the chat
collapse control lifts out of the chat card header to sit left of it.
Design files: the category tab bar drops its near-opaque sticky backdrop
strip and scrolls with content.
* feat(web): polish top-right capsule and dock controls, seed default deck chip
- Home composer seeds εΉ»η―η (deck) as the default creation type on a
fresh mount β same silent deferred-apply path as a user pick, skipped
whenever a persisted chip draft owns the composer.
- Type fan: tighter card spacing (arc pivot 3700px β 3000px, ~128px
between neighbours) so the hand-of-cards overlap reads again.
- Top-right chrome: credits + avatar merge into ONE capsule (pill
material on the shared wrapper, chrome-free 24px segments inside,
no hover fill on the credits segment).
- Project dock: the chat-collapse control gains a white seat matching
the tab dropdown, and the dropdown drops its border (shadow only).
* β¦
Why
While evaluating a Design System 3.0 application flow, the current
DESIGN.md, token, and component assets proved useful as prompt context but could not deterministically map a business intent to a reusable component, variant, properties, and required states. Agents still had to infer that relationship and could silently invent a near-match.This PR is the contract-and-validation foundation for the planned three-PR rollout. It adds an opt-in machine-readable runtime graph while preserving the existing prompt-based path for every package that does not declare it.
What users will see
There is no UI change. Design-system package authors can opt into a
runtimeblock inmanifest.jsonthat points to component, intent, lint, and fallback files. Declared graphs are parsed and cross-validated; malformed graphs are reported as invalid instead of silently falling back. ExistingDESIGN.md-only and v1 manifest packages continue to behave as before.Surface area
apps/weborapps/desktop(including Electron menu bar)odsubcommand or flag, newtools-dev/tools-pack/tools-prflag, or newOD_*env var/api/*endpoint, new SSE event, or changed shape inpackages/contractsskills/,design-systems/,design-templates/, orcraft/, or change to the skills protocolTRANSLATIONS.mdfor the locale workflow)package.json(dependenciesordevDependencies); workspace-packagepackage.jsonfiles are out of scope. Include a paragraph on what we get vs. what bytes we ship (seeCONTRIBUTING.mdβ Code style)Screenshots
Not applicable: this PR adds package contracts, daemon loading, validation, and tests without changing a UI surface.
Bug fix verification
Validation
pnpm installpnpm guardpnpm typecheckpnpm --filter @open-design/contracts testβ 38 files / 262 tests passedpnpm --filter @open-design/daemon exec vitest run -c vitest.config.ts tests/design-systems/runtime.test.tsβ 5 tests passedpnpm test tests/scripts/check-design-system-manifests.test.tsfrome2e/β 18 tests passedpnpm --filter @open-design/daemon buildThe full daemon suite was not used as the acceptance signal because local agent-CLI integration cases depend on host-installed Kimi/OpenCode behavior; the changed design-system paths are covered by the focused daemon tests, manifest guard, workspace typecheck, and daemon build above.