* feat(landing): add Codex Slides sister-product page
Add /codex-slides/ under the Product menu next to HTML Anything and
HTML Video, for the open-source AI slide studio nexu-io/codex-slides.
- Reuses the sibling product-page shape (hero, at-a-glance band, ideas,
numbered flow, deck gallery, product-UI gallery, scenarios, export
spec, Fast mode + install, closing CTA band).
- Full 11-locale copy: English base + Agent translations, metadata and
body localized; JSON-LD (SoftwareApplication + Breadcrumb + FAQ) uses
the localized page URL.
- Closing CTA reuses the shared .dbfinal painted band and leads with the
Open Design desktop download; ghost CTA stars the Codex Slides repo.
- Wires the entry into the Product mega-menu and the footer product
column; ships local webp screenshots (no R2 upload step).
* fix(landing): give the Fast mode / Install block its own section heading
The two-up Fast mode + Install in Codex notes had no heading of their
own, so they read as if they belonged under the preceding export section.
Wrap them in a proper section titled duoTitle (Fast and frictionless)
with the two notes demoted to sub-headings.
---------
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
* fix(daemon): preflight incompatible Codex models
* fix(daemon): fail open for layered Codex config
Detect system and enterprise config layers before rejecting a known-old Codex model, and recheck cloud config after the auth probe.
Isolate the capability integration suite from inherited Codex auth and endpoint variables with case-insensitive snapshot and restore coverage.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* feat(updater): manual cache clear + installer-reinstall floor on the installed outer version
Two disaster-recovery capabilities for the packaged updater:
- Reuse control.launcher.version.min properly: the gate now compares the
physically installed outer package version (read at check time from the
outer bundle's open-design-config.json via the launcher launch path;
OD_UPDATE_INSTALLED_VERSION overrides for tests), not the running payload
version. When the floor trips - or min is set but the outer version is
unreadable (conservative) - the updater selects the installer artifact and
offers a same-version installer reinstall when no newer release exists
(clamped to min <= candidate so the offer always converges). The status
snapshot carries a reinstall block {reason, installedVersion, minVersion,
url}; UpdateDialog/UpdaterPopup render reinstall copy with an optional
operator jump link. tools-release publish-metadata publishes the block from
RELEASE_LAUNCHER_VERSION_MIN(_URL) (per-channel repo vars with a _STABLE
fallback wired in all release workflows) and hard-fails when min exceeds
the release version; verify-metadata re-checks the published block and
summary-metadata surfaces the floor.
- Manual clear-cache action (od:update:clear-cache IPC, sidecar clear-cache
action, Settings > About row with a two-stage inline confirm): resets
one-shot update state (downloaded release, install freeze) back to idle,
purges releases/staging/downloads/.back, removes a stale launcher
attempt.json plus any non-confirmed desktop-handoff journal, and deletes
non-retained payload versions. Runtime active/lastSuccessful versions,
retained cleanup entries, install.json, and a confirmed handoff journal
are never touched; locked files defer through the existing cleanup.json
retry machinery.
Also: tools-pack --update-action now validates through sidecar-proto's
exported isDesktopUpdateAction so clear-cache reaches the ops surface;
tools-serve updater fixture gains control min/url knobs and CLI flags;
9 new i18n keys across all 19 locales; tools/pack/AGENTS.md documents the
gate semantics and clear-cache boundaries.
Validated locally per the updater matrix (desktop/web/tools-serve/tools-pack
suites, guard, typechecks) plus a mac release-beta closed loop: platform full
spec, same-version reinstall against a real outer/payload version split,
clear-cache recovery, and a real-feed payload update to 0.15.1-beta.4.
* test(updater): close Windows recovery acceptance
* test(updater): mirror Windows same-version reinstall recovery on the mac lane
Port the Windows runSameVersionUpdaterRecoveryAcceptance shape into the mac
full profile: after the payload cold-start acceptance, restart the packaged
app against a fixture publishing an installed-outer floor at the running
payload version and assert the same-version installer reinstall snapshot
(reason/installedVersion/minVersion/url), the popup reinstall learn-more
link, clear-cache back to idle with launcher pointers untouched, explicit
re-check recovery, and the installer open in dry-run mode — macOS has no
silent DMG install to execute, unlike the Windows NSIS transaction lane.
Extends the popup eval expression/validator with reinstallLinkVisible and
records an updaterRecovery block in the smoke summary. Full-profile test
timeout raised 240s -> 360s for the extra app restart.
Validated locally: release-smoke full profile green in 80s with the recovery
summary populated (reinstall block exact, cleared idle, recovered dmg,
dryRun install).
* refactor(release): centralize launcher version floor channel policy in one resolver
Channel policy for control.launcher.version.{min,url} is now managed as one
repo-vars pair per channel (RELEASE_LAUNCHER_VERSION_MIN[_URL]_<CHANNEL>).
Workflows pass the channel pair plus the STABLE pair through verbatim — the
|| fallback expressions are gone from YAML — and the shared resolver in
tools/release/src/storage/launcher-version-floor.ts owns resolution and
validation: pair-level stable fallback for non-stable channels (never mixing
one channel's min with another's url), version format, http(s) url, orphan
url rejection, and the floor <= releaseVersion guard. publish-metadata and
verify-metadata consume the same resolver, so verify re-derives the expected
block from the same channel policy instead of trusting a pre-resolved env.
release-workflows.test.ts pins the verbatim passthrough keys in all five
release lanes and rejects any reintroduced YAML fallback.
* feat(updater): let clear-cache rebuild a provably-owned corrupt update store
A corrupt store is one of the blocking scenarios the manual clear exists to
recover from, yet the action previously bailed on the very store errors it
should heal. clear-cache now rebuilds the update root when — and only when —
ownership is provable: the current-generation sentinel is the proof, and
everything else inside an owned root is updater cache by definition. Roots
without a sentinel, with a foreign-generation marker, or failing IO are never
touched; the original store error surfaces unchanged. Four specs pin the
boundary (corrupt metadata rebuilt, stray entries rebuilt, unowned root
refused with files intact, foreign-generation marker refused).
* fix(launcher): pre-arm delegated payload launches so pre-bookkeeping crashes roll back
A payload that spawned successfully but died before reaching its own launcher
bookkeeping left no attempt.json behind, so every later cold start re-selected
the same broken active pointer and re-delegated to the dead payload — a
permanent dead loop the installer fallback could never surface (found while
designing the crash-rollback acceptance).
The delegating parent now arms attempt.json BEFORE spawning the payload and
passes the pointer as --od-launcher-delegated-* argv. Selection gains an
active-delegated reason: an attempt matching active AND the delegated pointer
is the launch in progress, not a previous failure; only an exact active match
qualifies, so a stale pointer cannot defeat the rollback. Rollback
(last-successful) delegations deliberately carry no marker and never re-arm —
the attempt on disk IS the rollback evidence the child must re-derive.
Covered end to end: packaged outer delegation pre-arms (and skips pre-arm on
rollback), the updater's payload activation pre-arms and threads the pointer
through the deferred relaunch, and confirm clears the attempt for delegated
launches. launcher-proto 18, packaged 237, desktop 264 all green.
* fix(updater): scrub a stale payload install freeze after a rollback
A payload install freezes the updater with installResult.activeVersion
awaiting the relaunch. When that payload crashed and the launcher rolled
back, the app restarts on the OLD version with the freeze still persisted —
every later check became a frozen no-op, so a rolled-back install never saw
another update offer (the crash-rollback acceptance exposed this in its
self-heal phase). Cold-start restore now detects the broken promise —
installResult.activeVersion newer than the running version on a live process
— and drops the freeze and stale install result while keeping the verified
downloaded release actionable. The installer-artifact freeze (no
activeVersion) keeps its existing restore semantics. Red spec:
'clears a stale payload install freeze when the promised relaunch never
became the running version'.
* test(updater): full-lifecycle acceptance — silent startup, crash rollback, coverage map
Two new packaged acceptance tests on both platform lanes (mac validated
locally; win symmetric for the Windows box):
- 'applies a downloaded payload silently on the next cold start': enables the
daemon-owned allowSilentUpdates through the production GET+PUT
/api/app-config path, restarts, and asserts the first scheduler tick
installs, quits, and relaunches with converged pointers — no updater action
issued.
- 'rolls back a crashing payload and self-heals on the next good update':
repacks the real payload archive with a crash-before-bookkeeping stub
(mac: shebang script via ditto; win: a real executable — System32 where.exe
— via the installed app's bundled 7z, since a script cannot stand in for an
exe), drives the update through the popup, asserts the pre-armed attempt
survives the crash as rollback evidence, cold-starts into the rolled-back
base version (degraded steady state pinned: broken pointer stays active
with its attempt), then self-heals via a version-bumped healthy payload —
real recovery releases ship as version+1.
Spec hygiene: resetPackagedRuntimeState wipes launcher/runtime/daemon state
before each lifecycle test (uninstall keeps runtime data by design, and
leftover pointers silently delegate fresh installs onto stale payloads);
mac waitForHealthyDesktopVersion gains win's requireSettledLauncher opt-out
for rollback waits.
docs/testing/updater-lifecycle.md is the new lifecycle-to-test coverage map
(incl. deliberate manual-only nodes), linked from tools/pack/AGENTS.md and
the e2e coverage status doc.
* fix(updater): harden Windows full-lifecycle acceptance
* test(release): make channel-prepare hermetic against local git tags
prepare-preview derives its latest-stable floor from the clone's real
open-design-v* tags, so the channel-prepare tests were green on tagless CI
checkouts but permanently red on any developer clone once shipped stable
tags advanced past main's package version (0.15.1 vs v0.15.1/v0.16.0 today).
The spawned scripts now run with GIT_DIR pointing at a throwaway repo
holding exactly the tags each test declares, and a new negative case pins
the floor rejection itself (packaged version below a v99.0.0 stable tag).
* test(e2e): model delegated payload launch
Pass the updater's pre-armed payload pointer into the cross-runtime launcher simulation so it represents the spawned payload process instead of a later crash-recovery launch.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* test(web): wait for question form readiness
Wait for the asynchronously derived submit-disabled state before asserting it, removing the suite-order race observed by the Web workspace tests check.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(updater): preserve recovery state across locks and restarts
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
---------
Co-authored-by: Looper <looper@noreply.github.com>
* chore(scripts): make root scripts/ test-free and purify the guard entrypoint
Root scripts/ is now source-only, enforced by a new "scripts test-free"
guard check (any *.test.{ts,tsx,js,jsx,mjs,cjs} under scripts/ fails
pnpm guard). The root guard script becomes a pure tsx ./scripts/guard.ts
invocation instead of chaining a hand-maintained node --test file list.
Script behavior-contract coverage follows the boundary e2e/AGENTS.md
already declares for e2e/tests/scripts/: the fork PR approval security
tests migrate there (converted to Vitest, 24 tests passing), and the
remaining script unit tests are deleted — five of them (batch-design-
system-test, check-design-system-package-quality, plugin-previews-diff,
plugin-previews-gc, migrate-to-plugins/example) were orphans no runner
ever executed; the guard-chained checker tests covered logic whose
repo-wide enforcement remains in guard.ts checks. Coverage with a real
test claim gets rebuilt in e2e/tests/scripts/ on demand.
* test(ci): pin characterization goldens for scopes.ts scope detection
Golden tests run scripts/scopes.ts as a subprocess exactly the way ci.yml
does, with gh stubbed through the existing OPEN_DESIGN_GH_NODE_SCRIPT seam
(hermetic stub materialized per run), and pin the complete 20-key output
plan for a matrix of changed-file sets across pull_request,
workflow_dispatch (hot/full), and merge_group events.
They deliberately pin current quirks (skills/*.md arming daemon+web without
the ui-critical fallback; e2e/ui/*.md arming ui_p0 with workspace validation
off; dispatch-hot skipping the workspace_validation re-derivation) so the
upcoming rule-table restructuring can prove behavior preservation
byte-for-byte.
* refactor(ci): restructure scopes.ts into a confidence-tiered rule table
Scope detection becomes data plus a pure evaluator: every changed file is
classified by an additive rule table (union of matched effects, minimum
confidence across matched rules), and an evaluation context brings a trust
threshold — files below it, or matching no rule, escalate fail-closed to
the full radius. Today's behavior is exactly the degenerate case of this
structure (all rules medium, certain set empty): pull_request/hot believes
everything, the merge queue believes nothing and stays full. The
characterization goldens pass unchanged, byte-for-byte.
The exempt surface becomes an explicit empty-radius rule instead of a
special case, so exemptions can be split, rated, and promoted per rule.
Promotion to "certain" requires a guard field naming the enforcing check
(enforced by a rule-table invariant test).
New offline entry points for rule iteration: `plan --context
pr|merge-queue|full --files ...` evaluates without any GitHub environment
and prints { plan, trace }; `rules` lists the table. Every evaluation now
returns a decision trace (per-rule hit counts, per-file escalations, and
an ifTrustAll shadow plan that shows what medium-confidence rules would
have allowed — the evidence stream for future certain-tier promotions);
github-output mode logs it and writes a step-summary table without
touching the frozen 20-key output contract.
The legacy dispatch-hot asymmetry (no workspace_validation re-derivation)
is preserved behind an explicit option and pinned by the goldens.
* feat(ci): evaluate merge-group union diffs at the certain threshold with shadow trace
The merge queue now resolves the queued group's union diff
(compare base_sha...head_sha from the merge_group event payload) and
evaluates it through the rule table at the "certain" trust threshold.
While the certain rule set is empty every file escalates and the queue
keeps running everything — the goldens pin this byte-for-byte — but the
evaluation's ifTrustAll shadow column now records, for every queued
group, what medium-confidence rules would have allowed: the evidence
stream that future certain-tier promotions will cite.
Queue lanes derive from the evaluated outputs (ci_mode stays reported as
"full", but lanes are no longer unconditionally forced), so promoting a
rule to "certain" later is a pure rule-table change with no further
structural work.
Fail-safe behavior is explicit and tested: resolution errors and empty
resolutions fall open to the full plan without failing the scopes job
(queue throughput must never depend on this evaluation), while
pull_request resolution failures keep failing the run as before.
Changed-file resolution no longer filters out removed files (manual hot
runs previously did): deleting a runtime source file must trigger the
same validation scopes as editing it. This is the only behavior change,
and it is strictly fail-closed.
* fix(ci): preserve guard contracts and fail closed on compare limits
Restore the eight guard behavior suites in the e2e script lane and make merge-group compare resolution treat GitHub's 300-file ceiling as incomplete.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(ci): enforce extension-agnostic script test boundary
Match every suffix after .test. and cover TypeScript, MTS, and non-test filenames in the e2e script suite.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(ci): route script contract tests to e2e
Ensure changes under e2e/tests/scripts arm E2E Vitest with the same daemon and web scope effects as their root script counterparts, and pin the behavior with a PR golden.\n\nGenerated-By: looper 0.11.0 (runner=fixer, agent=codex)
---------
Co-authored-by: Looper <looper@noreply.github.com>
* feat(landing): add curated Codex design collection under /plugins
Add a standalone /plugins/codex-design/ collection of the best real,
external skills for doing design with OpenAI Codex (openai/skills,
MengTo/Skills), positioning Open Design as the home for designing with
Codex and driving to the client download + the awesome-codex-design list.
- New collection page + 6 flagship skill detail pages (+ locale wrappers)
- Featured Codex design banner on the /plugins hub
- Editorial content in _lib/codex-design.ts (every entry links to source)
- On-brand illustrations (Figma-selection language) for hub cover, hero,
and each skill
English baseline; per-locale copy layered in a follow-up.
* fix(landing): center Codex design collection + kill oversized section gaps
The collection page rendered full-bleed against the left edge and the
global `section { padding: 130px 0 }` rule blew the vertical gaps wide
open on both the collection and detail pages. Constrain the collection
to the shared 1180px safe area, neutralize the section padding on both,
and de-duplicate the hero eyebrow.
* style(landing): align Codex design collection to the site's editorial card DNA
The collection reinvented a soft, rounded, coral-accented card style that
clashed with the rest of /plugins/*. Rebuild it on the established
system: reuse the tpl-card DNA (1.5px ink border, hard offset hover
shadow, media bleed, mono uppercase labels, dark full-width CTA), the
shared .plugins-* hero primitives, and the ink/bone palette. Move the
skill grid to a global .codex-skill-grid (a scoped rule failed to apply),
fix the cramped detail-page related cards to full tpl-cards, drop the
off-palette coral from UI chrome, and de-duplicate the hero eyebrow.
* style(landing): make Codex design skill grid 4-up
Denser 4-column grid (was 3), matching the plugin hub's explore grid;
detail-page related row stays 3-up. De-dup the hero eyebrow.
* style(landing): regenerate Codex design illustrations brighter and punchier
The first pass read washed-out and samey at card size. Regenerate all
eight (hub cover, hero, six skill covers) with vivid acid-green + coral
accents, higher contrast, and crisp ink outlines so the covers pop in
the 4-up grid and match the site's acid-green accent.
* feat(landing): curated Codex design plugin catalogue (50 entries)
Rebuild the /plugins/codex-design collection from 6 placeholder entries into
50 curated, real, installable design skills and plugins for Codex and other
coding agents.
Each entry carries copy grounded in the upstream SKILL.md, its source repo,
star count, licence, a Codex-compatibility note, and a traction signal
(GitHub stars plus X/YouTube), cross-checked across GitHub, X and YouTube.
- 50 flat /plugins/<slug>/ routes with per-locale wrappers (11 locales)
- 50 covers: 14 upstream first-party showcase images, 36 generated
- collection page gains a category filter; detail card shows stars, traction,
and a bundle note for multi-module entries
- 13 generic sub-command names disambiguated as 'Parent - Subname'
- full 11-locale localisation of UI and all 50 bodies
- README refresh plus new hero for the codex-design repo lives separately
Licence discipline: openai/skills (no root LICENSE), anthropics/skills (no
LICENSE), MengTo demo imagery (third-party sourced) and design-dna imagery
(real third-party sites) were all excluded.
* chore(landing): trigger landing CI (no-op comment)
* fix(landing): address review on the Codex design catalogue
Copilot review follow-ups:
- drop the committed GitHub repo-stats snapshot (data, lib, and fetch
script). It carried an openai/skills row that contradicted the stated
licence discipline, and a checked-in stats file goes stale silently.
The detail card now shows only the star count that lives with the
catalogue entry itself.
- remove the unused sourcePath frontmatter local (dead code).
- drop the .codex-related .codex-skill-grid override; the related row
renders under .dbrelated, so the selector never matched.
- the filter chip bar is a plain filter, not a tab set: role=group
instead of role=tablist, which promised tab semantics the buttons
never implemented.
- fix the stale route in the hallmark page header comment.
- delete the detailForks / detailContributors / detailUpdated i18n keys
that the removed stats card was the only consumer of.
* style(landing): let the source card stat row flow by column
The row now carries a single stat, so a fixed three-track grid was
describing a layout that no longer exists.
* fix(landing): stop tracking the landing-page node_modules symlink
A local symlink named node_modules was committed with the catalogue: the
root ignore rule is `node_modules/`, and a trailing slash matches only
directories, so a symlink of that name slipped past it. It pointed at an
absolute path on the author's machine, so every CI job died in setup —
pnpm cannot mkdir through a dangling link:
ENOENT: no such file or directory, mkdir '.../apps/landing-page/node_modules'
Untrack it and drop the trailing slash so the rule covers a file or
symlink of that name too.
* fix(landing): drop the Figma category and announce the active filter
Review follow-ups on the Codex design catalogue:
- The catalogue has no plugin that reads Figma. The official Figma
skills are excluded on licence grounds, so the sole entry filed under
"Figma to Code" was image-to-code, an image-first workflow that
generates its own reference images and never connects to Figma. The
chip therefore promised "read a real Figma design" and led to a
plugin that cannot do it. Move that entry to Frontend & UI, drop the
category from the union type, the filter, and the copy, and reword the
lede and intro from three jobs to two.
- Announce the filter state: the selected chip lived only in the visual
is-on class, so a screen reader heard plain buttons and could not tell
which filter was active. Initialise aria-pressed and keep it in step
with is-on in the click handler.
- The locale files still advertised the old "6 plugins / 3 workflows"
figures from the six-entry version; correct them to 50 and 13 across
all ten.
* fix(landing): put the catalogue's source-card prose into English
Follow-ups from the second review pass:
- codexNote, social, and bundle are rendered verbatim by the detail
template, but 57 of those values were written in Chinese, so English
and every other locale showed strings like 'README 提 Codex ×8' on the
source card. Translate them in place; the data module is the English
source these pages fall back to.
- The plugin hub banner still sold 'Figma pipelines' in all eleven
locales after the category was dropped. Reword it to match.
- The jobs band allocated three fixed desktop columns and now holds two
entries, leaving an empty third. Flow one equal column per category so
the band tracks whatever CODEX_COLLECTION.categories holds.
* fix(landing): name the catalogue cover links for screen readers
The cover image is decorative (alt=""), so the only text left inside
each card's media link was the category chip: all 37 frontend covers
announced as "Frontend & UI" and all 13 system covers as "Design
Systems", with no way to tell which plugin a link opened. Label each
link with the localized skill name, matching what the shared
template-card already does, on both the collection grid and the related
row of the detail page.
---------
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
* fix(web): dedupe project workspace tabs on repeated open
The openWorkspaceTab event handler unconditionally appended a new tab on
every project-open event, so reopening the same project stacked duplicate
tabs instead of focusing the existing one. Mirror the dedup logic already
in syncStateToRoute: when a project tab with the same projectId exists,
update its conversationId/fileName/lastActiveAt and activate it rather
than appending.
Fixes#2641
* fix(web): coalesce duplicate project tabs on workspace restore
normalizeTabsState deduplicated entry tabs and tab ids but not project
tabs sharing a projectId, so workspaces persisted before the dedupe fix
restored with their pre-existing duplicate project tabs still visible.
Coalesce project tabs by projectId in normalizeTabsState (keep the active
match, else the newest), mirroring the entry-tab dedupe. This repairs
affected persisted state on restore, not just new corruption.
Adds a restore regression with two distinct tab ids for the same project.
Follow-up to #2641 / review feedback on #5993
buildBlocks() in AssistantMessage filters a fixed list of internal status
labels before they reach StatusPill, but tool_call / tool_call_update are
not in it, so they fall through and render as raw, expandable status pills
with no tool name, input, output, or detail — looking like "a tool ran but
produced no output".
These are transient ACP markers: on the live SSE path the daemon already
normalizes them to `running` (TRANSIENT_ACP_STATUS_LABELS in
providers/daemon.ts), which buildBlocks() skips. The persisted-events path
does not normalize, so the bare labels survive into the UI. Add both labels
to the existing skip list, with a comment pointing at the daemon source of
truth.
Add a red-spec test asserting the two empty status rows render no pill, plus
a guard that status rows carrying a real detail still render.
* fix(message-center): honor active AMR profile for anonymous sync
* fix(vela): keep control context on one config snapshot
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* feat: support Codex service tiers
* fix: forward service tiers through MCP runs
* fix: clear default service tier selections
* fix: preserve service tier on model option updates
* fix: preserve service tier without explicit model
* fix: preserve service tier in connection tests
* test: stabilize API empty stream UI check
* fix: preserve codex service tier labels
* fix: preserve service tier without saved model
* fix: address service tier review follow-ups
* fix: pin legacy chat assistant rows before start
* fix: accept bare model catalog responses
`stable_prompt_cache_miss_reason='stable-prompt-changed'` says THAT the
cached instruction prefix moved between turns, never WHICH input moved.
Diagnosing a drift has meant pairing Langfuse traces by hand and eyeballing
two multi-KB prompts for a diff, so the dominant bucket in production --
4,673 of 6,612 AMR drift events over 14 days, on later-resume turns -- has
no explanation telemetry can give.
Hash each named input of the prefix per turn, store the map alongside the
existing hash on agent_sessions, and diff it when the hash no longer
matches. `run_finished` gains `stable_prompt_changed_sections`; the Langfuse
turn gains the same field.
Attribute over the composer's INPUTS rather than by slicing the composed
prompt on its `---` separators: the question a drift has to answer is what
CAUSED the prefix to move, and the cause is always an input -- splitting the
output would report which bytes differ but still leave the mapping back to a
cause to a human. It also keeps prompt assembly untouched.
stablePromptHash stays the only source of truth for hit/miss; section hashes
never gate a re-send, so a section-table bug can mislabel a drift but never
suppress or fabricate one. A drift no tracked section explains reports
`unattributed` -- a coverage-gap alarm rather than a silently clean turn,
which is what keeps the table honest as new inputs enter the prefix.
The composeSystemPrompt argument literal is hoisted to a const and passed
unchanged, so one object both composes the prompt and feeds attribution
instead of a second hand-maintained copy that would drift from the real
inputs and mislabel the telemetry it exists to explain. No prompt bytes
change.
Installer-managed processes can respawn while NSIS replaces the active executable. Quarantine the launch path and preserve the previous installation until system registration succeeds.
Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>
* give the unattributable client failures a real error code
Three buckets in the daily stability report name nothing, and in every case the
evidence was already in scope at the emission site:
- BYOK connection test: 801 failures across 201 devices in 24h — 10% of ALL
connection tests — reported `result.kind` verbatim, so every response the
daemon's status→kind map had no case for collapsed into `unknown`. The user
is told "connection failed" and we cannot say whether it was an out-of-credits
402, a self-signed cert, or a base URL pointing at an HTML login portal.
`ConnectionTestResponse` already carried `status` and a redacted `detail`;
only the emission site threw them away.
- Frontend exceptions: the Top-5 table literally renders a row as
`Error — 235 次 / ~74 台`. Two independent sources — `buildExceptionList`
copied `error.name`/`error.message` straight through and both can be '', and
`parseStack` returned a zero-frame stacktrace for a stackless error, which
PostHog drops at ingestion so neither `$exception_type` nor
`$exception_value` survives.
- Export failures (8.1%, 210/24h): everything that was not a sidecar-skew
message fell through to `err.name`, i.e. the literal "Error".
The daemon side of the same path flattened every sidecar contract violation to
`UPSTREAM_UNAVAILABLE`, which forced the web layer to regex-match English prose
to tell a version-skewed mesh from a dead renderer; it now forwards the
sidecar's real code, and the web classifier treats the remaining generic
envelope codes as non-classifications so a specific message still wins.
Two existing export-code expectations changed on purpose and say why inline: a
generic envelope `.code` no longer outranks message classification, and a
message carrying an HTTP status now reports that status instead of "Error".
* keep the forwarded sidecar code inside ApiErrorCode
Follow-up on the same PR. Forwarding the sidecar contract violation's `code`
compiled, but it was leaving the declared union: `ctx.http.sendApiError` is
typed `(...args: any[]) => any` in server-context.ts, so nothing caught that
`SIDECAR_UNKNOWN_MESSAGE` is not an `ApiErrorCode`. Every web-side handler
typed against that union would have failed to match the code the daemon
actually put on the wire.
Declare the two SIDECAR_* codes in API_ERROR_CODES and narrow the helper's
return type to ApiErrorCode, allowlisting exactly those two so a future sidecar
code cannot silently leave the union again.
No consumer of the export 502 switches on the old code — the UPSTREAM_UNAVAILABLE
call sites are finalize (useFinalizeProject), AMR run guidance, and ChatPane run
errors, none of which are on this route — so this is additive, not a behavior
change for anything that exists today.
* make the attribution actually fire on the production paths
Three review findings, all correct, all verified against the real code paths
rather than the synthetic ones the first cut tested.
1. The sidecar code passthrough was dead. `requestJsonIpc` rejects with
`new Error(response.error?.message)` — its response type does not even
declare a `code` — so SIDECAR_UNKNOWN_MESSAGE never reaches the route, and
the helper always fell back to UPSTREAM_UNAVAILABLE. Removed the passthrough
(and the two API_ERROR_CODES entries added for it: nothing emits them). The
message-based classifier already achieves the attribution and is tested;
dead code that reads as working is worse than no code. Threading the code
through IPC → API → web export result is a three-layer change and belongs in
its own PR.
2. The zero-frame guard checked the input instead of the output. A header-only
stack ("Error: Script error.") is truthy, so it skipped the fallback, and the
parser then dropped that single line as a message — yielding `frames: []`,
the exact ingestion failure this change exists to prevent. The guard now runs
on the PARSED result, covering missing / empty / header-only / unparseable
stacks, with a fixture matrix over all four.
3. The certificate bucket could not fire in production. undici reports a TLS
failure as a TypeError whose message is the generic `fetch failed`, with the
reason only on `cause.code`, and `networkErrorToKind` maps just an allowlist
— so a self-signed cert became `kind: unknown` + `detail: "fetch failed"`.
The unit test passed only because it was fed a detail string production never
emits. The daemon now appends the cause code to `detail`, and the client
tests assert against that real shape.
* stop a 2xx status from claiming a failure it cannot explain
Review catch, and it made this PR's headline case unreachable. The status
fallback ran before every detail classifier, so the daemon's
`{ kind: 'unknown', status: 200, detail: 'Unexpected token < …' }` — which is
exactly the 'base URL points at an HTML login portal' shape, returned when a 2xx
body fails JSON.parse (connectionTest.ts) — was reported as HTTP_200, and
INVALID_JSON_RESPONSE could never fire.
Reproduced before fixing:
byokErrorCode({kind:'unknown',status:200,detail:'Unexpected token < …'})
→ 'HTTP_200'
The cut is now at 300 rather than 0. A 2xx that still failed did so for a reason
the status cannot express, so it falls through to the detail classifiers; 3xx
stays on the status side because the daemon fetches with `redirect: 'error'`,
making a redirect a real failure the status does name.
* read the daemon-appended cause code instead of enumerating codes
Review catch on a gap my own previous commit opened: forwarding cause.code from
the daemon put OpenSSL's TLS codes on the wire, and this file could not read
them. UNABLE_TO_GET_ISSUER_CERT_LOCALLY, CERT_NOT_YET_VALID and CERT_HAS_EXPIRED
have no Node-errno shape, and the `\bcertificate\b` fallback never matches a
name that spells it CERT — so the signal was preserved on the wire and dropped
here, landing back in UNKNOWN_NO_SIGNAL.
Read the daemon's known trailing `(CODE)` position directly rather than
extending an allowlist: an enumeration would silently drop every code added
upstream after this file was written, which is the same failure mode one layer
down. Inline codes keep a widened pattern, and the TLS fallback now also matches
CERT-shaped names.
Fixtures cover all seven codes the daemon can now forward.
* name every packaged startup failure instead of bucketing it as unknown
The weekly `packaged_runtime_failed` alert is dominated by failures the event
had enough evidence to name and reported as opaque anyway. Triaging a 7d window
turned up three places where the emitter threw away a cause it was already
holding:
- 62 devices died with the daemon log tail read successfully, but
`parseDaemonLogTail` only ever matched `ERR_*`. A daemon that threw anything
else (SQLite corruption, EPERM, a plain throw) reported error_code=null AND
missing_module=null with its reason printed in the tail we had just read.
- 21 devices hit win32 CreateProcess failures. Node puts the real cause on the
error's `code`/`errno`/`syscall`; the event sent `.message` only, so
`spawn UNKNOWN` was indistinguishable from any other opaque failure.
- 149 devices produced an Error with an empty `.message` and no `.stack`. Every
free-form field went null, leaving residue that could not even be counted
apart from "no error at all".
So: parse the sidecar's own fatal line as a backstop when `ERR_*` misses, carry
the errno triplet off the thrown object, classify a rejected spawn as its own
`spawn-failed` kind rather than `unknown`, and guarantee a thrown error never
serializes to error_message=null — an unnamed leftover is now a named, countable
bucket.
Classification-only; the failures themselves are unchanged. Every test input is
a verbatim field payload from the alert window.
* reduce syscall to its operation token before sending it
Review catch, and a real leak. Node does not guarantee `syscall` holds only the
operation name: spawning a missing binary sets it to the whole invocation.
Verified against real Node —
spawn('/Users/elian/nonexistent/bin/thing')
→ syscall === 'spawn /Users/elian/nonexistent/bin/thing'
Forwarding that verbatim would have put the local username and install path
into an event that is retained even for opted-out users, while every other path
this module sends goes through scrubUserPaths.
Keep the first token — the operation is the whole analytic value, and the path
is already available (scrubbed) via error_message — then scrub what survives
and cap it, as a second line of defence. `code` is capped too.
This also fixes a classification miss the same review implies: the spawn-failed
branch compared the RAW syscall to 'spawn', so a spawn error carrying a path
never matched and fell through to `unknown` — the exact bucket this PR exists
to drain.
* fix: preserve dark-first brand canvases in derived themes
A brand extracted with an explicitly dark background role and a light
foreground role previously rendered as a white kit: seedFromBrand
clamped any background below 0.72 luminance to #ffffff, and
deriveTokens re-clamped the seed and hardcoded #141414 for dark mode.
Every dark-first source (near-black app UIs, dark portfolio sites)
produced a default theme that looked nothing like the brand, and
re-running finalize reverted agent-made dark rethemes.
seedFromBrand now preserves the true canvas when the brand carries a
confidently dark background (or surface) plus a light foreground, and
deriveTokens honors a dark seed across all three algorithms: the
default theme keeps the brand canvas with dark palette math, and the
dark algorithm uses the brand's own near-black instead of the generic
fallback. Ambiguous brands (mid-gray canvas, missing roles) keep the
light baseline unchanged, and the programmatic URL fast pass
(seedFromMaterial) intentionally stays light.
* fix: align exported theme.json algorithm with dark-first tokens
Review follow-up: the derived tokens, variables.css, and kit.html use dark
palette + neutral math for a dark-first seed, but both theme.json producers
still serialized tokensToThemeJson(seed, 'default'). An Ant ConfigProvider
consumer would then apply the light algorithm to the dark canvas, diverging
from the rest of the generated system.
Add defaultThemeAlgorithm(seed) as the single shared decision (dark-first
-> 'dark', light/ambiguous -> 'default'), exported from the engine and used
by both the build.ts and system.ts (finalize) theme.json paths. Assert the
exported algorithm for both the dark-first and ambiguous cases so the
ConfigProvider artifact can't drift from the kit/CSS again.
* fix: gate dark-first predicate on dark bg AND light fg
Second review follow-up (@nettee): seedPrefersDark() keyed only off
colorBgBase, but neutralBases() (the extraction-path gate) requires a dark
background AND a light foreground. The rebuildSystem seed-override path
(sanitizeSeedOverrides) can merge a background-only override like
{ colorBgBase: '#050505' } onto an otherwise light brand, which flipped
deriveTokens to the dark ladder (forcing white text) while
tokensToThemeJson still serialized the seed's dark colorTextBase — the same
ConfigProvider mismatch under overrides instead of extraction.
Make seedPrefersDark mirror neutralBases exactly (dark <= 0.3 AND light
>= 0.6) so a background-only override stays on the light/default path
consistently across deriveTokens, defaultThemeAlgorithm, and theme.json.
Add a regression locking the override seed both ways (bg-only -> default,
bg+light-fg -> dark).
---------
Co-authored-by: wiggdevin <wiggdevin@gmail.com>
* fix(i18n): localize BYOK provider titles to English
Five BYOK provider titles in apps/web/src/state/config.ts were hardcoded in
Chinese while the rest of the provider list used English-facing names:
qwen 千问 -> Qwen
volcengine 火山引擎 -> Volcengine Ark
qianfan 百度千帆 -> Baidu Qianfan
mimo 小米 MiMo -> Xiaomi MiMo
zhipu 智谱 -> Zhipu AI
These were rendered into the Settings > Execution mode > BYOK provider chip
list verbatim, producing a mixed-language list when the UI was set to
English (issue #5693). The providerLabel for each was already in English;
only the user-facing title was inconsistent.
The new titles mirror the providerLabel phrasing where the existing
providerLabel read naturally as a standalone name (Qwen, Volcengine Ark,
Baidu Qianfan, Zhipu AI) and use a friendly "Xiaomi MiMo" for mimo where
the providerLabel carried extra "OpenAI" suffix not appropriate for a chip
title.
No i18n catalog entries needed — these were never translated, the title
field is the display string.
Fixes#5693
* fix tests: update BYOK provider tab labels to English
Two test assertions in apps/web/tests/components/SettingsDialog.execution.test.tsx
hardcoded the old Chinese tab names that were just localized to English in
the BYOK_PROVIDER_PRESET_SPECS array (commit 13037c5):
- "小米 MiMo" -> "Xiaomi MiMo" (line 1190)
- "智谱" -> "Zhipu AI" (line 1628)
Without these test updates the Web workspace tests job fails because the
tab element with the old Chinese accessible name no longer exists — the
config change renamed the chip title and the tab role name derives from it.
* fix(web): lift home context picker secondary text to a legible token (#4468)
The home context picker (the @-mention surface above the prompt) drew its
secondary text — per-tab counts, section labels, option meta, and the
hover-card kicker — in `--text-faint`. In dark mode that token (#4e4b46)
sits at ~1.8:1 against the picker panel, far below WCAG AA, so the counts
and category labels read as illegible bleed-through.
Move those four selectors to `--text-muted`, matching the option
description that already uses it. The picker's secondary tier now clears
AA in dark mode (~5.3:1) and stays consistent across the tabs and list;
light mode improves from ~2:1 toward AA on the same selectors.
Adds a stylesheet-level contrast guard that resolves each selector's
color token and asserts WCAG AA against the surface it renders on.
* fix(web): extend picker contrast lift to the hover-card meta row (#4468)
Mirrors vela's TRIAL_CREDIT_PROMO_ENABLED (powerformer/vela#912): the
new-user signup trial-credit promotion is temporarily offline, so the
/pricing/ Free card must stop advertising it while keeping every string
in place for relaunch.
While the switch is false:
- the Free card drops its trial-credit benefit row and both hosted-model
lists (free hosted-model access, incl. the Grok trial pool, only exists
through the signup grant)
- the Free tagline swaps to a no-promo variant (10 locales)
- the FAQ drops the "what is the limited-time trial credit" entry and
swaps the plans-differ answer to a trial-free variant (10 locales)
Flipping the switch back to true restores all promo copy untouched.
Claude-Session: https://claude.ai/code/session_01SaoLc5kR92BffzDP8acfrA
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
* feat(daemon): route completed-run telemetry through Vela
* fix(daemon): migrate test telemetry relay domain
* feat(daemon): send Vela feedback scores with completed-run telemetry
Map score-create into the Vela envelope and use the same Vela sink for
thumbs feedback, binding scores via the client run id instead of skipping
them when account-aware telemetry is selected.
* fix(daemon): skip anonymous fallback for Vela feedback auth errors
Score-only feedback batches must stay on the account-scoped Vela path.
When Vela returns 401/403, fail the delivery instead of posting to the
anonymous relay, which has no matching trace for those scores.
Generated-By: looper 0.11.0-beta.1 (runner=fixer, agent=grok-build)
* fix(daemon): normalize Anthropic-compatible BYOK base URLs
* test(e2e): stabilize home plugin details flow
Wait for Home project loading to settle before revealing templates, then target the semantic Details control after verifying actionability so layout shifts cannot collapse the gallery or trigger the wrong card action.
Generated-By: looper 0.10.4 (runner=fixer, agent=codex)
* fix(daemon): version Anthropic gateway URL paths
Append /v1 through the parsed URL pathname so gateway query parameters and fragments remain in their proper URL components. Preserve the prior string-concatenation fallback for invalid URLs.
Generated-By: looper 0.10.4 (runner=fixer, agent=codex)
* reconcile run terminals after restart
* fix(daemon): preserve replayed failure taxonomy
Reclassify analytics-incomplete terminal failures from their persisted state and events while keeping the interrupted template exclusive to daemon-restart recovery.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(daemon): preserve repaired errors and skip completed logs
The repair path previously collapsed every failed message to restart copy and hydrated event logs before checking completed telemetry checkpoints. Use durable recovery metadata for status detail and gate log reads on outstanding PostHog or Langfuse work.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(daemon): bound macOS watcher file descriptors
* fix(daemon): preserve symlink roots and refresh Nix hashes
Keep the watch-root exemption ahead of the symlink traversal guard and lock the behavior with a real directory-symlink assertion. Refresh the daemon and web pnpm dependency hashes reported by the failing Nix build.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(landing-page): keep locale variants of proper-noun blog posts out of the index
The marp / slidev / reveal.js posts each rank for a single navigational
product-name query. Those queries are language-neutral, so Google ranks all
ten translated copies against the same English SERP. Over 2026-07-01..07-18
the thirty localized URLs took ~96k impressions and returned 19 clicks — a
0.02% CTR — while accounting for the entire site-wide impression surge that
pushed average CTR from 14% down to 3.7%. The Spanish copies were outranking
the English originals (/es/blog/slidev/ 27,465 impressions vs 3,431).
Add an opt-in `noindexLocaleVariants` frontmatter flag. When set, the
localized variants emit `noindex, follow` (outbound equity still flows) and
the hreflang cluster is dropped from every variant including English, since
hreflang must not advertise noindexed URLs. English stays indexable.
SeoHead grows a page-level `noindex` prop, kept distinct from the existing
site-wide staging flag: staging continues to emit `noindex, nofollow` for the
whole mirror, verified unchanged by an OD_LANDING_NOINDEX=1 build.
* refactor(landing-page): give the hreflang suppression its own prop
`localeCanonicalOnly` is documented as "this page has no per-locale
variants" and also drives the language switcher and locale auto-redirect in
sub-page-layout. These posts do have locale variants, all thirty are built
and remain reachable through the switcher; only their hreflang advertisement
has to go. Borrowing the flag happened to produce the right HTML today
because SeoHead ignores the switcher half of its meaning, which is exactly
the kind of coincidence that breaks on the next refactor.
Split out `noindexedLocaleVariants`. Output is byte-identical: 30 variants
still emit `noindex, follow`, the three English posts still carry no robots
meta and zero non-x-default hreflang, the control post keeps its 11.
---------
Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
* fix(daemon): keep canceled runs canceled after late agent errors
* fix(daemon): preserve failures that precede cancellation
Record whether stream and ACP failures were observed before a cancel request, and let close-time classification win over the cancel fallback in that ordering. Add a startup smoke case that posts cancel only after the provider error is visible.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* 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>
* fix release: notarize prerelease mac builds
* fix(release): inject Apple notary env on prerelease mac retry steps
The cache-miss retry steps for both mac_arm64 and mac_x64 prerelease
builds pass --notarize but lacked the APPLE_ID /
APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID env block present on the
primary build steps. notarize.cjs fails closed when those vars are
missing, so a recovery build could not produce a notarized candidate.
Mirror the primary step env on both retry steps and assert the notary
env appears twice per mac job in release-workflows.test.ts.
Generated-By: looper 0.10.3 (runner=fixer, agent=claude-code)
---------
Co-authored-by: Looper <looper@noreply.github.com>