Commit Graph
420 Commits
Author SHA1 Message Date
Tom HuangandGitHub ad5a7d6ba1 Revert "feat(web): add guided inspiration and deliverable workflow (#5899)" (#6120)
This reverts commit 340cbb9b94.
2026-07-27 14:02:48 +00:00
Ray XiandGitHub c3de4d5b95 fix(daemon): classify model and timeout failures (#6103) 2026-07-27 08:56:35 +00:00
PerishFireandGitHub a63eca6a4a fix(desktop): cover clear-cache in updater IPC teardown and host bridge guard (#6101)
* fix(desktop): include clear-cache in updater IPC teardown and host bridge guard

The od:update:clear-cache handler was registered but missing from
UPDATER_IPC_CHANNELS, so runtime teardown never removed it and a second
createDesktopRuntime() in the same process would throw on re-registration.
The host bridge structural guard likewise accepted an updater bridge that
lacks the clear-cache method, deferring the failure to call time.

Red specs: the boundary test now derives the teardown table from the
registered od:update:* handlers, and the host contract test rejects a
bridge missing clear-cache.

* refactor(desktop): deduplicate deferred-installer timeout and update action literals

The mac and Windows deferred-installer timeouts were two constants with
one value branched on platform; fold them into one. Route handle()'s
action dispatch through DESKTOP_UPDATE_ACTIONS instead of repeating the
string literals.

* docs(pack): correct the beta-s acceptance feed path to the betas channel

release-beta-s publishes RELEASE_CHANNEL=betas, so the internal feed
lives under betas/latest, not beta/latest.
2026-07-27 08:31:18 +00:00
340cbb9b94 feat(web): add guided inspiration and deliverable workflow (#5899)
* feat(cli): add project rename command to CLI

Introduced a new command `od project rename <id> --name "<title>"` to allow users to rename existing projects. The command validates input and sends a PATCH request to update the project name in the backend. Updated CLI help documentation to include this new command.

* feat(web): enhance InspirationPicker with reference-site shortcuts and selection chips

Added functionality to the InspirationPicker component, allowing users to browse reference sites (e.g., Dribbble, Behance) directly from the picker. Implemented a selection chips feature that lists all current picks, enabling easy removal across different families. Updated styles for better visual representation of selected items. Enhanced internationalization support with new translations for the added features.

* feat(web): enhance InspirationPicker with detailed previews and selection management

Updated the InspirationPicker component to include a detailed preview feature for selected items, allowing users to view full documents directly from the picker. Implemented a "Selected" section that displays current picks with a remove option, improving user interaction. Adjusted styles for better visual consistency and added new translations for enhanced internationalization support.

* fix(web): restore uploaded inspiration thumbs and drop preview elapsed

Resolve locked inspiration-upload previews via the submitted path map so
answered forms still show real images, and keep the floating preview
status pill as a stage label only.

* feat(web): implement TaskDeliverableCard for displaying task outputs

Added a new TaskDeliverableCard component to showcase the primary deliverable and its associated outputs. This includes a preview feature for various file types, an actions menu for sharing and downloading, and improved styling for better user experience. Enhanced internationalization support with new translations for deliverable-related terms. Updated InspirationPicker to utilize the new deliverable ranking and searching functionalities.

* fix(inspiration): unify design system previews and source priority

* fix(web): align deliverable card regression coverage

* test(web): scope question form assertions

* fix(web): keep the deliverable card off failed and canceled runs

The card's status label is an unconditional Done, so gating it on the
shared terminal predicate let a failed or canceled run that wrote files
present a partial artifact as delivered while the same message reported
the error. Gate on the existing runSucceeded predicate instead; those
turns fall back to the plain file ledger, so the files stay reachable
without the false completion signal.

* fix(daemon): serialize DESIGN.md font stacks through a strict font-family grammar

extractFonts is deliberately permissive and its output was interpolated
verbatim into the generated <style> raw text, so a Body font value like
"Karla</style><img src=…>" could close the style element and inject
active markup into every auto-loaded card iframe. sanitizeFontStack now
rebuilds each stack from validated family names only (quoted names,
unquoted generic/system keywords), dropping anything else and falling
back to the trusted default stack; both the full preview and the compact
card renderer route every font interpolation through it. Card and
preview fixtures with a </style> payload pin the boundary.

* fix(daemon): ground additional inspiration systems in the run prompt

The picker's extra design-system picks reached the agent as ids on a
metadata line only, so a user-authored system contributed none of its
colors, fonts, or rules. The daemon now resolves each additional id
through the same DESIGN.md reader chain as the primary (workspace file
first, then registry), bounds the result (3 systems, 6000 chars each),
and composes an 'Additional inspiration systems' section with explicit
primary-wins precedence. Prompt tests pin a unique secondary token
reaching the composed prompt, both at the composer seam and end-to-end
through the chat route with bento's #FAD4C0.

* test(e2e): stabilize preview comment click

* test(web): update runtime override history expectation

---------

Co-authored-by: Amy <1184569493@qq.com>
2026-07-27 05:01:57 +00:00
b99a9fdc3d feat(manual-edit): add direct manipulation and reliable history (#5890)
* feat(web): manual edit v2 direct manipulation (v2.2–v2.4)

Selection overlay with action bar and drag/resize handles, floating text
toolbar with range-format highlight (queryCommandState round trip), Manus
color picker, image replace/crop/paste/drop, element copy/duplicate/delete,
and the systematized in-place content pipeline: every element-scoped patch
now reconciles the live iframe DOM from the saved source over
od-edit-apply-dom (replace/insert-after/append-child/prepend-child/remove)
with positional identity restamping, so commits, undo and redo never reload
the canvas, never flash, and never move the scroll position. Frozen-source
reloads remain only as a scroll-capturing fallback. Includes the Chromium
Home/End double-booking fix, the dblclick word-selection guard, and light
targets-broadcast dedupe.

* test(web): manual edit v2 unit and e2e coverage

Bridge (in-place apply ops, positional restamp, selection format state,
Home/End consumption, dblclick selection guard), source-patches (insert
readback, restore descriptors, sanitization), gestures/shortcuts/color
units, and host-level FileViewer suites proving content commits mutate the
canvas without a srcDoc reload, hand selection to inserted elements, and
record labeled manual versions for undo/redo. Also realigns the
manual-edit-history and srcdoc-reload-races helpers with the v2.1 opt-in
inspector two-step flow.

* docs(specs): manual edit v2 architecture spec

Six-layer map (identity/chrome/style/content/history/external-sync), core
invariants (DOM equals saved source without reloads; positional identity
restamps on structural change; every save is a labeled manual version),
patch-kind application matrix, performance contract, and the v2.1–v2.4
revision log including the real-browser verification round.

* fix(tools-dev): fail fast when a sidecar process dies during startup wait

waitForDaemonRuntime/waitForWebRuntime now accept a liveness probe for the
spawned pid, so a crashed daemon/web process surfaces immediately instead
of burning the full startup timeout.

* fix(web): edit brand-kit pages in place — no reload flash, no scroll jump

Brand-kit targets carry runtime-annotated ids with no markup of their own in
the saved source (edits persist into the payload / runtime overrides), so
the in-place readback came up empty and every commit fell back to a
srcDoc reload — the flash-and-jump-to-top the user hit when editing text
after an image. New od-edit-apply-dom op 'apply-content' mirrors the patch
fields (text/href/src/alt/html/attributes, on*/data-od-* filtered) onto the
live element instead, and set-outer-html reuses replace with the patch
html. The reload fallback's scroll restore also gains a clamped-only retry
ladder (600–4000ms) so async-rendering pages (brand kit renders after
load) no longer land at the top when a reload is genuinely needed.

* refactor(FileViewer): unify manual edit history management and enhance undo/redo functionality

This update introduces a consistent approach to managing manual edit history and undo/redo stacks by utilizing refs to mirror state. The new `updateManualEditHistoryStack` and `updateManualEditUndoneStack` functions ensure that the state remains in sync with the refs, preventing stale data issues during undo/redo operations. Additionally, the handling of in-session undo (Cmd+Z) has been improved to escalate to the host's global history when no local changes remain, ensuring a seamless user experience. Tests have been added to verify the correct behavior of these features.

* feat(manual-edit): enhance manual edit functionality with space scaling and improved gesture handling

This update introduces a new `ManualEditSpaceScale` interface to manage the scaling between viewport pixels and CSS pixels, ensuring accurate positioning during manual edits. The `manualEditMovePreviewTransform` and `manualEditResizeStyles` functions have been updated to utilize this scaling, improving the precision of drag and resize gestures. Additionally, the `ManualEditSelectionOverlay` component now maintains pointer capture during gestures to prevent interaction issues with nested iframes. Tests have been added to verify the correct behavior of these enhancements, ensuring a smoother user experience in manual editing scenarios.

* feat(FileViewer): add localized toasts for image upload process in manual edit

This update enhances the manual editing experience by introducing localized toast notifications for the image upload process. New messages for uploading, processing, and successful addition of images have been added to the i18n files. The `insertManualEditImage` function now manages these notifications, providing users with real-time feedback during image uploads. Additionally, tests have been implemented to ensure the correct display of these toasts across different locales.

* fix(manual-edit): drag whole image to move + lock selection frame to element box (#5888)

Two direct-manipulation fixes reported from real prototype/landing/deck edits:

1. Move by dragging the image body, not only the top pill. Adds a
   full-interior moveBody surface inside the selection frame that starts the
   same undoable move gesture; scoped to image targets so text/link bodies
   keep click-to-edit. Rendered under the resize handles so edges still resize.

2. Keep the selection frame locked to the element during resize. The frame
   previously drew the gesture-intent x/width and only adopted the iframe's
   measured y/height, so an element that re-centers, caps at max-width, or
   reflows on width change left the frame drifting horizontally off the
   picture. adoptMeasuredExtent now takes the full measured rect and
   displayRect follows it, so the chrome always wraps the element.

Adds ManualEditSelectionOverlay tests for body-drag move and measured-box
frame sync; records the change as spec v2.5.

Co-authored-by: Claude <noreply@anthropic.com>

* test(web): stabilize manual edit browser validation

* fix(manual-edit): address structural edit review blockers

* fix(manual-edit): preserve runtime formatting and restore order

* fix(manual-edit): flush toolbar styles before navigation

* fix(manual-edit): preserve runtime target identity

* fix(manual-edit): serialize saves and image handoff

* fix(manual-edit): clear stale runtime clipboard

* fix(manual-edit): scrub rich text identities

* fix(manual-edit): save inspector text after formatting

* fix(manual-edit): action-bar boundary avoidance, select-without-reflow, image paste shortcut (#5892)

Three edit-mode canvas fixes from user feedback:

1. Action bar no longer covers the element frame/handles. A small or
   edge-hugging element previously had its move pill and resize handles
   hidden under the floating action bar (clamped to the top), so it could not
   be moved or resized. New pure helper manualEditActionBarTop places the bar
   above the frame, flips it below when the element hugs the top edge, and
   keeps it inside the canvas; horizontal clamping already handles the side
   edges. Adds a data-placement attribute.

2. Selecting a text element no longer makes it jump/grow. The bridge fused
   'select' and 'enter inline edit' into one click, and making a deck text box
   contenteditable releases its line-clamp/height cap so it visibly reflows
   larger the instant it is selected. The first click now only selects (no
   contenteditable, no reflow); editing begins on a second click of the
   already-selected element or a double-click — matching how images/containers
   already select.

3. A selected image/container can be pasted with Cmd/Ctrl+V. The native paste
   event only fires while an editable element holds the caret, which never
   happens for non-text selections, so the shortcut did nothing for images.
   The bridge keydown handler now forwards od-edit-paste-request for a selected
   non-editable element (paired with the existing Cmd/Ctrl+C copy).

Updates the 14 inline-edit bridge tests to enter edit via double-click and
adds coverage for all three fixes; recorded as spec v2.6.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(manual-edit): image corner scaling, nowrap unlock on resize, restore image paste

Three edit-mode canvas fixes from user feedback:

1. Images get four corner anchors that scale aspect-locked from any corner,
   anchored at the opposite corner (new resize-nw/ne/sw/se gesture kinds).
   The dominant pointer axis drives the scale, the floor binds the larger
   dimension, and corner gestures skip snapping so the ratio never breaks.
   Corner resizes persist width AND height (space-scale corrected) plus the
   offsets that keep the anchored corner fixed.

2. Resizing an element with white-space: nowrap (or pre) now unlocks
   wrapping (normal / pre-wrap) alongside the width — a narrowed heading
   previously stayed on one line and overflowed its own new box.
   ManualEditStyles and the bridge style props gain whiteSpace.

3. Image paste works again. The v2.6 Cmd/Ctrl+V keydown branch called
   preventDefault, which suppresses the native paste event — the only place
   clipboard image bytes are readable — so pasting an image did nothing.
   The shortcut now arms a 150ms one-shot element-paste fallback instead;
   any real paste event disarms it and routes image vs element itself, so
   the two channels never double-fire.

Adds gesture/overlay/bridge coverage for all three; recorded as spec v2.7.

* fix(manual-edit): replay runtime history in place

* fix(brands): reject scraped CSS junk font families before token emission

An extraction can capture CSS source text instead of a real font family
(e.g. Tailwind v4's `--theme(--default-font-family` from aliyun.com).
Interpolated raw into the kit's `--brand-font-family`, the unbalanced
parenthesis swallows every later :root declaration, so the component kit
renders unstyled (UA serif, collapsed buttons) while earlier color tokens
still work.

isRenderableFontFamily() now guards both emission seams: the seed's
fontStack (kit/variables/artifacts token block) and brandFontAssets'
display-family stack (h1-h3 face). Junk families drop out and the
renderable fallbacks + system stack take over.

* fix(manual-edit): replay runtime-only style history from the persisted override rule

Runtime-only brand-kit targets persist set-style patches in the
style[data-od-manual-edit-runtime-overrides] rule, not as element inline
styles. The in-place history replay read inline styles only, so redo of a
runtime style change replayed empty values while reporting success — the
file held the redone style while the canvas stayed on the undone one.

readManualEditSavedStyles() now reads a target's persisted styles wherever
they live (inline attribute for source-backed nodes, the runtime override
rule otherwise), and the set-style history branch replays through it. The
same inline-only read also made reconcileManualEditStyleSave() treat every
runtime style save as "target vanished" — erroring, dropping the selection,
and reloading the canvas — so it now consults
hasManualEditRuntimeStyleOverride() before taking that path.

Regressions: source-patches unit coverage for the saved-styles read and the
override predicate, plus a FileViewer spec that styles a runtime target,
undoes and redoes it without any srcdoc change, and asserts the redo
preview carries the persisted value (red on the previous head).

* fix(manual-edit): merge partial runtime style patches into the persisted rule

Successive debounced toolbar autosaves send only the touched property, but
setRuntimeStyleOverride() rebuilt the target's override rule solely from the
incoming patch, so a runtime-only brand-kit target lost its earlier saved
declarations (fontSize save, then color save -> font-size silently dropped).
Persistence now merges the incoming patch into the existing rule
declarations, treating empty incoming values as deletions and removing the
rule once its last declaration is deleted.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 04:09:02 +00:00
Marc ChanandGitHub 7787556cf1 fix(daemon): full ACP tool transcript and richer usage telemetry (#6057)
* fix(daemon): full ACP tool transcript and richer usage telemetry

Map all ACP tool_call frames to canonical tool_use/tool_result for
run.events, PostHog run_finished, and Langfuse spans. Preserve
artifact_count via Write/Edit normalization, tighten path extraction,
forward truncated results, and emit real tool startedAt.

Enrich usage scanning (thought_tokens, anthropic vs openai cache
semantics), canonical tool-family PostHog props, analytics recovery
finish snapshot, and shared content-tool redaction.

* fix(daemon): prefer ACP kind over noncanonical tool names

Normalize recognized ACP kinds (e.g. kind:read + name:read_file → Read)
before emitting tool transcripts so Langfuse content-tool redaction matches
canonical families and does not leak file contents.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): fail-closed redaction for unknown ACP tool payloads

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): flush open ACP tools as errors on session fail

When a session dies mid-tool (timeout, exit, RPC error), emit pending
tool_use/tool_result pairs with isError so transcripts stay complete.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): redact ACP Bash stdout in tool transcripts

Execute tools can dump private files (e.g. cat .env); replace Bash
tool_result content with a length summary before the canonical transcript
ships so Langfuse cannot receive raw execute output.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): redact Linux home paths in Langfuse Bash tool spans

Extend local-path masking for ACP Bash partial redaction to cover
/home and /root (not only /Users and Windows Users), so Linux home
paths do not leak into Langfuse tool inputs when content telemetry is on.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): redact custom ACP tool names before Langfuse telemetry

Sanitize path/token-like kind:other tool names at transcript emit and
canonicalize non-allowlisted names for Langfuse span labels, toolName
metadata, and redaction placeholders so untrusted custom names cannot
leak even when content telemetry is off.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* test(e2e): force-click unstable comment targets in preview iframe

UI P0 project-workspace timed out on comment attachment when Playwright
kept waiting for hero-title stability under auto-fit zoom / bridge inject.
Click once visible with force so the critical flow stays reliable.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): merge partial usage frames in reverse analytics scan

Keep provider_usage for thought/cache-only trailing usage events while
continuing the reverse scan to the latest primary input/output/total
record so run_finished and Langfuse do not drop complete token counts.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): flush open ACP tools before AMR no-output fail

Clean-flush pending non-think tools on successful session/prompt before
evaluating emittedConcreteToolEvent so missing terminal tool_call_update
does not mark a productive run as acp_no_visible_output.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): merge partial primary usage and flush tools on ACP abort

Require both input and output before ending the reverse usage scan so
output-only or input-only trailing frames still merge earlier counters.
Flush deferred open ACP tools as errored on abort so user-canceled runs
keep complete tool transcripts for Langfuse/PostHog.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): hash path-like ACP toolCallIds before telemetry

Adapters can embed user paths in toolCallId (e.g. read:/home/alice/.env).
Hash those values at tool_use/tool_result emit so Langfuse span ids and
metadata.toolCallId cannot leak secrets, while keeping the raw id as the
local frame-correlation map key.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): always hash ACP adapter toolCallIds for telemetry

Identifier-shaped secrets (sk-proj-*, ghp_*, JWTs) passed the prior
allowlist because they contain no path characters. Hash every non-empty
adapter toolCallId before tool_use/tool_result emission so Langfuse span
ids and metadata.toolCallId cannot leak tokens.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): keep reverse usage scan open for missing cache fields

When a newer usage frame already has input+output but omits cache
counters, continue scanning older frames so cache_read/cache_creation
still merge into run_finished and Langfuse.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): retain flushed ACP tool IDs against late terminals

Keep acpToolRunEventState entries after flushOpenAcpTools so a racy
tool_call_update cannot recreate the same id and emit a second
tool_use/tool_result pair after timeout, abort, or prompt flush.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): block kind:other Bash-like tool names

Custom ACP tools with kind:other must not emit Bash/shell/execute/terminal
names, which would unlock Langfuse partial redaction and leak rawInput.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)

* fix(daemon): fail closed on Bash names without execute kind

ACP tools missing kind must not preserve name/title Bash labels that unlock
Langfuse partial redaction; only recognized execute-family kinds may emit Bash.

Generated-By: looper 0.11.0 (runner=fixer, agent=grok-build)
2026-07-27 04:08:27 +00:00
netteeandGitHub de2020fb88 docs: reconcile code-evolution drift (#6083)
* docs: reconcile code-evolution drift

* fix(web): preserve generic-disconnect recovery retries

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

* fix(web): retry capped reattach probe after cleanup

Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
2026-07-27 03:03:45 +00:00
a2ba3ee2ad feat(updater): manual cache clear, installer-reinstall floor, and crash-recovery lifecycle (#6032)
* 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>
2026-07-24 05:18:48 +00:00
open-design-release-bot[bot]GitHubopen-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
ca4ecf0bf2 chore(release): bump main to 0.16.1 ahead of stable 0.16.0 (#6000)
Co-authored-by: open-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
2026-07-23 03:15:11 +00:00
dcdb0c4204 fix packaged updates after payload cwd cleanup (#5967)
* fix packaged payload startup after cwd cleanup

* fix deferred installer helper cwd

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

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-07-22 08:15:45 +00:00
jaehanbyunandGitHub 2337932714 feat: expose GPT-5.5 Fast service tier (#4287)
* 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
2026-07-22 07:11:03 +00:00
lefarcenandGitHub 2e377a5152 name every packaged startup failure instead of bucketing it as unknown (#5956)
* 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.
2026-07-22 06:20:33 +00:00
PerishFireandGitHub bb6c79c86d fix packaged Windows obsolete outer cleanup (#5955) 2026-07-22 03:50:42 +00:00
PerishFireandGitHub 447b18b98e fix packaged mac relaunch blocked by obsolete outer (#5940) 2026-07-21 15:01:26 +00:00
Marc ChanandGitHub 1226773663 feat(daemon): route completed-run telemetry through Vela (#5666)
* 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)
2026-07-21 13:48:54 +00:00
Ray XiandGitHub d1372da029 fix(daemon): reconcile run terminals after restart (#5817)
* 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)
2026-07-21 10:02:42 +00:00
6fb644a57d feat(analytics): report conversation turn index (#5917)
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-21 09:44:51 +00:00
Ray XiandGitHub 4e8db9a9b7 add runtime version and retry provenance (#5814) 2026-07-21 09:23:36 +00:00
lefarcenandGitHub ef35a08e24 feat(daemon): E-lite root-cause telemetry for inactivity_timeout attribution (#5775)
* feat(daemon): E-lite root-cause telemetry for inactivity_timeout attribution

Add four run_finished fields that separate WHY a stalled run died, which
the coarse `last_observed_phase` (WHICH phase) cannot:

- approval_requested: an ACP approval/permission gate fired. Only the ACP
  path is daemon-observable (stream/CLI runtimes pass a skip-permissions
  flag so no gate fires); surfaced via an acp_approval_request diagnostic
  and folded in by summarizeRunDiagnosticsForAnalytics.
- stdin_backpressure: the prompt write to the child's stdin was queued
  because the OS pipe buffer was full (the child was not draining stdin) —
  the corroborating signal for a stdin_write-phase stall.
- tool_result_sent: a tool_result came back for the run's LAST tool_use.
  A stall with tool_call_seen && !tool_result_sent is the
  tool-result-not-delivered root cause.
- last_progress_age_ms: age of the last agent activity at finish (the
  inactivity-watchdog clock). Near the ceiling on a stall, ~0 on a clean
  finish.

Contract fields on RunFinishedProps; daemon populates two off the run
object (stdin_backpressure, last_progress_age_ms) and derives two from
run.events in the diagnostics summarizer (tool_result_sent,
approval_requested).

* fix(daemon): pair tool_result_sent by id so parallel tools attribute correctly

Review catch (nettee/Looper): the first cut flipped `tool_result_sent` to
true on any tool_result after a tool_use, so a multi-tool turn like
tool_use(A), tool_use(B), tool_result(A) reported delivered even though B
was still outstanding — bucketing multi-tool stalls as "provider stalled
after delivery" instead of "tool result not delivered".

Now paired by id (`tool_use.id` <-> `tool_result.toolUseId`, the same
pairing summarizeRunTimingAnalytics uses): `tool_result_sent` is true only
when EVERY committed tool_use received a matching tool_result (outstanding
set empty). Tests cover the reviewer's regression case plus parallel
all-resolved and sequential-last-outstanding.

* fix(daemon): count-pair id-less tool events so degraded stalls aren't "delivered"

Second review catch (nettee/Looper): id-based pairing alone still fell
through to `tool_result_sent: true` when a runtime emits a tool_use with a
null id, because those were counted in `sawAnyToolUse` but never tracked as
outstanding. That is not hypothetical — `agent-protocol/pi-rpc/events.ts`
and `copilot-stream.ts` both emit `toolCallId ?? null` for tool_use.id AND
tool_result.toolUseId on degraded provider events, so an unpaired id-less
tool call would mask exactly the stall this metric attributes.

Id-less tool events are now paired by count alongside the id-keyed set:
`tool_result_sent` requires both an empty outstanding-id set and at least as
many id-less results as id-less uses. Tests cover an unpaired id-less stall
and an id-less pair that did resolve.

* fix(daemon): capture stdin backpressure on the default text-input path

Third review catch (nettee/Looper): `stdin_backpressure` never recorded
anything for non-`stream-json` runtimes. `child.stdin.end(composed, ...)`
returns the stream rather than a boolean, and `writableNeedDrain` is already
back to false by the time it returns — even for a chunk that `write(chunk)`
would have rejected. Claude is the only runtime on the stream-json path, so
the field was permanently false on exactly the runs whose `stdin_write`
stalls it exists to attribute.

The write and the close are now issued separately through
`writePromptAndEndStdin`, which returns the boolean the OS pipe actually
gave us. Extracted into chat-run-lifecycle.ts so it is unit-testable:
tests cover the backpressured write, the accepted write, and the flush
callback still firing (the `stdin_write_end` lifecycle mark). Verified red
against the previous behavior.
2026-07-21 06:34:31 +00:00
lefarcenandGitHub c9e622d0a9 fix(daemon): ship baseline OpenCode for Windows and stop retrying AVX2 illegal-instruction crashes (#5733)
* Classify AVX2 illegal-instruction agent crashes as cpu_unsupported and stop retrying them

The Windows opencode.exe bundled with the AMR runtime is a Bun build that
requires AVX2. On CPUs without it (Intel Atom/Celeron/Pentium N-series
through 2021) the binary dies with STATUS_ILLEGAL_INSTRUCTION before
readiness; vela surfaces an ACP fatal, the daemon classified it as a
retryable fatal_rpc_error, and the automatic retry deterministically
failed again. 88 of 105 pre-readiness Windows crash traces in Langfuse
are this single failure.

The crash text (Bun 'Illegal instruction' banner with a no_avx2 CPU
feature line, or the raw 0xC000001D / decimal 3221225501 exit status) now
classifies as process_exit/cpu_unsupported ahead of the fatal_rpc_error
close-reason promotion, is never auto-retried, and renders a dedicated
error card telling the user to update Open Design instead of a dead
Retry button.

* Bump @powerformer/vela-cli to 0.0.23 so packaged Windows builds bundle the baseline (non-AVX2) OpenCode

The 0.0.23 win32-x64 platform package compiles the OpenCode fork with
--target=windows-x64-baseline, so the bundled opencode.exe no longer
requires AVX2 and starts on Intel Atom/Celeron/Pentium N-series CPUs.
Verified in vela CI: the build log shows the baseline target and the new
post-publish smoke job boots the published bundle's opencode.exe on a
real Windows runner (powerformer/vela#838).

* Refresh Nix pnpm deps hashes for the vela-cli 0.0.23 lockfile change

* Add the abort-after-panic (exit status 3) production shape to cpu_unsupported specs

A user on an AVX-but-no-AVX2 CPU (Sandy/Ivy Bridge era) hit the same
illegal-instruction crash, but Bun panicked during its own panic handler
and abort()ed, so vela reported exit status 3 instead of
STATUS_ILLEGAL_INSTRUCTION. The stderr banner (no_avx2 / Illegal
instruction) is the only signal; encode the real Langfuse trace shape so
the classifier keeps catching it.

* Bump @powerformer/vela-cli to 0.0.25 so packaged Windows builds bundle the baseline (non-AVX2) OpenCode

0.0.25 is the first stable cut after powerformer/vela#838 merged: its
win32-x64 package compiles the OpenCode fork with
--target=windows-x64-baseline (verified in the release CI log), the
post-publish smoke boots the published bundle on a real Windows runner,
and the lockfile regenerator now pins the baseline asset so a routine
version bump cannot silently flip back. 0.0.24 predated that merge and
still shipped the AVX2 build.

* Tighten cpu_unsupported to precise signals only (no bare 'Illegal instruction')

Review is right that a bare illegal-instruction line is too broad: an
unrelated SIGILL on an AVX2-capable machine (runtime bug, corrupted
jump) would be mislabeled 'Processor not supported' and lose its retry.
Bun's crash banner always prints the CPU-feature line, so machines
actually missing AVX2 are identified by no_avx2; the bare Windows
STATUS_ILLEGAL_INSTRUCTION renderings (0xC000001D / 3221225501) stay
because those runs carry no banner at all. Generic illegal-instruction
banners without no_avx2 return to process_crashed / fatal_rpc_error,
locked by a new negative spec.

* Refresh Nix pnpm deps hashes for the vela-cli 0.0.25 lockfile change

* Gate raw STATUS_ILLEGAL_INSTRUCTION on the bundled-opencode startup context
2026-07-21 06:10:22 +00:00
df50cffbd2 fix(platform): discover Grok Build on Windows (#5843)
Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>
2026-07-21 02:43:13 +00:00
VikingOwl91andGitHub 7c742163c5 fix: support pnpm install --ignore-scripts with pnpm bootstrap (#5874) 2026-07-20 14:18:50 +00:00
Ray XiandGitHub d3e091e157 fix(daemon): classify deterministic failures and preserve retry attribution (#5882) 2026-07-20 14:00:02 +00:00
30a4c284c6 feat(web): extend visual style catalog to visual artifacts (#5746)
* feat(web): extend visual style catalog to visual artifacts

* feat(web): expand visual style catalog

* Simplify chat execution disclosures

* test: cover collapsed file disclosures

* refactor(web): consolidate chat task activity

* fix(web): show small file outputs inline

* fix(web): separate file outputs from reads

* fix(web): collapse completed todo cards

* refactor(web): compact completed todo lists

* fix(web): make completed todos expandable

* fix(web): keep four output files visible

* refactor(web): clarify chat activity timeline

* fix(web): make file output disclosure interactive

* feat(web): emphasize chat run states

* style(web): simplify chat run status colors

* style(web): remove chat run status icons

* feat(web): stream one chat activity at a time

* style(web): align chat completion row

* Revert "style(web): align chat completion row"

This reverts commit 06500ab651.

* Revert "feat(web): stream one chat activity at a time"

This reverts commit 396787af13.

* Revert "style(web): remove chat run status icons"

This reverts commit 37960751f0.

* Revert "style(web): simplify chat run status colors"

This reverts commit a33fa890d9.

* Revert "feat(web): emphasize chat run states"

This reverts commit c9368dddc9.

* Revert "fix(web): make file output disclosure interactive"

This reverts commit 2ee81afcdb.

* Revert "refactor(web): clarify chat activity timeline"

This reverts commit ebb86284ef.

* Revert "fix(web): keep four output files visible"

This reverts commit fa876bf452.

* Revert "fix(web): make completed todos expandable"

This reverts commit 172abf27c1.

* Revert "refactor(web): compact completed todo lists"

This reverts commit 972e94a333.

* Revert "fix(web): collapse completed todo cards"

This reverts commit b8b2472917.

* Revert "fix(web): separate file outputs from reads"

This reverts commit 93aa16c9a1.

* Revert "fix(web): show small file outputs inline"

This reverts commit 83c6044634.

* Revert "refactor(web): consolidate chat task activity"

This reverts commit 36375bbeac.

* Revert "test: cover collapsed file disclosures"

This reverts commit 99731894cd.

* Revert "Simplify chat execution disclosures"

This reverts commit e7c18b5889.

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-20 08:01:08 +00:00
888d35ce96 feat(desktop): add macOS check for updates flow (#5789)
* feat(desktop): add macOS check for updates flow

* fix(web): refine update dialog layout

* fix(web): refine update-ready copy

* fix(web): refine updater dialog states

* fix(desktop): map preflight denials to active-run copy, skip menu rebuild on progress ticks

Address review feedback on PR #5789: UpdaterPopup and Settings About now
surface the active-runs-blocked/active-runs-unknown restart-safety denial
as user copy instead of a generic failure, and the app-menu preflight
response no longer publishes a synthetic error onto the shared updater
status channel. The mac app-menu rebuild now compares a stable derived
menu-item key so download progress ticks stop triggering a full
Menu.setApplicationMenu rebuild.

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-07-20 07:47:35 +00:00
Ray XiandGitHub 2192a7f6b9 fix incomplete BYOK configuration preflight (#5745)
* fix BYOK configuration preflight

* fix(web): short-circuit non-BYOK run preflight

Local-agent test fixtures omit BYOK-only fields, but provider resolution read model before rejecting non-BYOK protocols. Return before touching those fields and keep the pending-prompt API fixture explicitly valid.

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

* fix(web): persist committed BYOK key clears

Treat a blank active-provider key as draft-only while it is being edited, then persist the clear once the field is committed. This preserves incomplete replacement drafts while invalidating saved account-scoped model state.

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

* test(web): align BYOK draft expectations

Keep incomplete model discovery edits in the provider-scoped draft while the active configuration remains unchanged, and expect the current DeepSeek preset default.

Generated-By: looper 0.10.4 (runner=fixer, agent=codex)
2026-07-20 04:26:54 +00:00
3447f60a34 fix packaged payload desktop handoff (#5678)
* fix packaged payload desktop handoff

* fix beta mac full smoke fixture wiring

* fix tools-pack mac payload cold start

* test payload desktop update restart

* fix Nix daemon launcher protocol build

* fix(pack): harden Windows long-jump updates

* fix(pack): harden mac long-jump acceptance

* fix(handoff): defer armed journal writes until spawn and shutdown succeed

Previously executeLegacyPayloadDesktopHandoff wrote the armed handoff
journal, attempt, and runtime state before spawning the payload desktop
and before the old desktop accepted shutdown. When spawn() threw or the
shutdown request failed, the armed journal was stranded on disk, so the
next cold start bailed out of prepareLegacyPayloadDesktopHandoff() with
reason "already-armed" and the install stayed pinned to the old desktop
generation indefinitely.

Move the three writes to after both the payload child has spawned and the
shutdown has been accepted. The old desktop is still alive while it acks
the shutdown, and the payload waits for the old pid to exit before
resuming, so the state still lands before the payload reads it. Add
regression coverage for both the spawn-failed and shutdown-failed paths,
asserting the journal stays "prepared" and a later cold start can retry.

Generated-By: looper 0.10.3 (runner=fixer, agent=claude-code)

* fix(updater): await payload spawn acceptance

* fix(packaged): advance confirmed handoff recovery

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-07-20 02:21:49 +00:00
netteeandGitHub 6b90486c97 docs: reconcile stale documentation with current code (#5829)
* fix(docs): align question guidance with form flow

* docs: reconcile stale documentation with current code

* fix(docs): remove retired template metadata from guides

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

* fix(e2e): stabilize visual workspace helper

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

* fix(docs): align localized template guide metadata

Generated-By: looper 0.10.6 (runner=fixer, agent=codex)
2026-07-18 03:41:08 +00:00
Ray XiandGitHub 4ddfc6e447 fix(media): retry transient image generation responses (#5702) 2026-07-17 09:12:36 +00:00
lefarcenandGitHub e49d1abc42 fix: resolve media-backed memory extraction keys (#5767)
* fix: resolve media-backed memory extraction keys

Generated-By: looper 0.0.0-dev (runner=worker, agent=codex)

* fix: refine unsupported memory provider detection
2026-07-17 08:46:08 +00:00
Christopher BeaulieuandGitHub 6bee94be03 fix(deploy): honor target preview/production for Cloudflare Pages (#4483) (#4576)
* fix(deploy): honour target for Cloudflare Pages branch and URL selection (#4483)

- deployToCloudflarePages: add optional target ('preview'|'production', default 'production')
- Branch derivation: preview→'preview', production/omitted→'main'
- URL selection: preview uses per-deploy alias; production uses production root
- routes/deploy.ts: read target from request body (default 'production'), pass to deployToCloudflarePages, persist actual target (not hardcoded 'preview')
- db.ts normalizeDeployment: read target from DB row instead of hardcoding 'preview'
- contracts: widen target fields to 'preview'|'production'; add target to DeployProjectFileRequest

* test(deploy): type FormData branch capture as string | undefined (#4483)

The Phase-1 branch-capture objects were typed `{ branch?: string }` but
assigned `string | undefined`, which fails `tsc` under
`exactOptionalPropertyTypes`. Type the field as `branch: string | undefined`
so the daemon typecheck gate passes; assertions are unchanged.

* fix(deploy): reject invalid deploy target with HTTP 400 (#4576)

The deploy route coerced any non-'preview' target to 'production', so a
malformed value (typo, empty string, wrong case) silently published to the
production Cloudflare branch instead of a gated preview. Validate the supplied
target up front and return 400 for anything outside preview|production; an
omitted target still defaults to production (non-breaking).

Addresses review feedback on #4576 / #4483.

* feat(deploy): add `od deploy` CLI subcommand with --target (#4483)

Land the CLI surface for the preview/production deploy capability so it is
not API-only. `od deploy <projectId> --file <name> [--target preview|
production] [--provider vercel-self|cloudflare-pages] [--cf-zone-id]
[--cf-zone-name] [--cf-domain-prefix] [--json] [--daemon-url]` POSTs to the
same `/api/projects/:id/deploy` endpoint the web UI uses, with local
validation of `--target` before any request (omitted target = no key, server
defaults to production).

Addresses the surface-closure review feedback on #4576 by shipping the
embeddability (CLI) surface alongside the API contract. The human web-UI
preview/production selector remains tracked as a follow-up in #4577.

Tests: apps/daemon/tests/cli-deploy.test.ts (11 cases, written test-first).

* feat(deploy): add web UI target selector for Cloudflare Pages deploys

Closes the web-UI surface gap PerishCode flagged on #4576: deploy target
(preview/production) had HTTP + od CLI support but no way to choose it
from the deploy modal. Adds a target <select> to the Cloudflare Pages
deploy panel, defaulted to Preview, threaded through deployProjectFile's
new optional target argument and forwarded in the deploy POST body.

References #4576.

* fix(deploy): reject vercel-self target=production instead of silently deploying preview

POST /api/projects/:id/deploy resolved the requested target but only
threaded it into deployToCloudflarePages(); deployToVercel() never
received it and always behaved as preview. A caller explicitly
requesting target=production against the Vercel provider was silently
deployed as preview with no error (Codex P2 finding on PR #4576).

Real Vercel production-target support is out of scope for this PR, so
reject the explicit request with 400 BAD_REQUEST instead. The guard
gates on the raw requested value, not the resolved target, so an
omitted target (which defaults to 'production' for Cloudflare Pages
parity) keeps deploying Vercel as preview exactly as before.

* fix(web): default Cloudflare Pages deploy target to production

The deploy target selector defaulted to 'preview', both in its initial
useState and when syncing from the (currently placeholder,
always-'preview') GET /api/deploy/config response. A user who opened
the Cloudflare Pages deploy modal and clicked Deploy without touching
the selector silently deployed to preview instead of production,
contradicting the daemon's documented default for an omitted target
and pre-PR behavior.

Default both sites to 'production'; explicit user selection of either
target is untouched. FileViewer.deploy-target.test.tsx covers the
default and both explicit selections.

Fixes #4483
2026-07-17 08:43:50 +00:00
Ethan GuoandGitHub c110e40e8d fix(frontmatter): parse flush-left YAML sequences, quoted inline arrays, and deep block scalars (#5499) 2026-07-16 07:34:41 +00:00
265aa51630 feat(web): make Studio discovery visual and inline (#5496)
* feat(web): make Studio discovery visual and inline

Move clarification forms into the chat flow, add artifact-aware visual style choices, and keep follow-up actions hidden until discovery is complete.

* feat(web): keep clarification entirely in chat

Remove the legacy Questions workspace tab, preserve discovery analytics in the inline form, and keep Skip, Submit, and answered summaries in one conversation flow.

* docs: split Studio Chat input and response work

Frame visual discovery as the input-side initiative and record response aggregation as a separately reviewed follow-up so the current PR stays bounded.

* docs: reorganize Studio Chat experience plans

Separate the overall journey, visual discovery, and response architecture so each scope can be reviewed and delivered independently.

* docs: define first-artifact satisfaction prompt

Limit the feedback prompt to the first successful previewable artifact so it gathers a useful signal without interrupting every iteration.

* docs: keep Studio Chat plans independent

Preserve the existing discovery proposal as one document and keep response architecture as a separate review surface without an extra master layer.

* fix(web): preserve inline discovery behavior

Keep visual direction single-select while restoring custom input, optional auto-continue, analytics, and current end-to-end coverage for the inline flow.

* fix(web): preserve files when skipping discovery step

* fix(web): exclude skipped file answers

* fix(web): align inline discovery with slim prompt

Preserve the latest discovery contract while keeping inline forms, uploads, drafts, and schema-driven visual selections consistent after the rebase.

* feat(web): add style catalog preview images

* fix(web): clarify discovery form completion

* fix(web): preserve question form context

* fix(web): time out unanswered question forms

* test(web): cover inline question form next steps

* fix(web): roll back partial inline uploads

* ci: trigger validation after rebase

* fix(web): unlock blocked inline question submits

* fix(web): retain previewless visual answers

* fix(web): load visual style previews from R2

* fix(web): clean blocked inline uploads

* test(web): expect R2 style preview assets

* feat(web): show full visual style catalog

* fix(web): keep inline question answers consistent

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-16 06:57:43 +00:00
66e4061274 fix(memory): retire chat auto-extraction and clean up its artifacts (#5708)
- chatExtractionEnabled now defaults OFF (explicit opt-in only); it gates
  the regex pack, chat-form profile capture, and LLM extractor together
  while leaving memory injection untouched
- add a one-time boot migration (memory-cleanup.ts) that deletes regex-pack
  artifacts by name+template fingerprint, prunes user_profile to the
  canonical field set the settings editor writes, and forces the legacy
  config flag off once; marker-gated so a conscious re-opt-in sticks
- persist writer provenance (frontmatter source:) on every new entry so
  future audits can tell auto-extracted facts from hand-written ones
- LLM-extracted entries are deliberately kept: on-disk they are
  indistinguishable from hand-written prose, and they are the least
  harmful of the retired pipelines' output

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
2026-07-16 02:53:44 +00:00
Nicholas-XiongandGitHub a219a933eb fix(memory): forward BYOK chat model through /api/memory/extract (#5177)
* fix: forward BYOK chat model to memory extraction (#5162)

Root cause: Two issues preventing non-OpenAI BYOK providers (e.g.
MiniMax) from working with memory-llm extraction:

1. Web side (ProjectView.tsx): The BYOK chatProvider snapshot sent
   to /api/memory/extract was missing the `model` field. Without it,
   pickProvider() fell through to PROVIDER_DEFAULTS.openai.model
   ('gpt-4o-mini'), which doesn't exist on non-OpenAI endpoints.

2. Daemon route (memory.ts): The /api/memory/extract POST handler
   did not extract body.chatModel, unlike every other memory route.
   Even when the web side passes a model, it was silently dropped.

Changes:
- ProjectView.tsx: Add `model: byokOpenCodeProvider.model` to the
  BYOK chatProvider snapshot so pickProvider() can use the user's
  actual chat model (e.g. 'MiniMax-M3').
- routes/memory.ts: Read `body.chatModel` and forward it to
  extractWithLLM options, matching the pattern used by all other
  memory routes.

Testing: typecheck passes for apps/web (affected packages). The
daemon typecheck requires pre-built @open-design/release which is
a separate toolchain concern.

* fix(contracts): add model field to ByokChatProviderConfig for BYOK memory extraction (#5177)

The web-side memory snapshot was reading byokOpenCodeProvider.model but
the BYOK config contract had no model property, causing a compile-time
type error. Add an optional model field so the daemon can forward the
user's chat-model picker choice to memory extraction.

* fix(byok): copy config.model into byokOpenCodeProvider snapshot

Address review comment on PR #5177: byokOpenCodeProviderFromConfig()
did not copy config.model into the returned ByokChatProviderConfig,
so byokChatProvider.model serialized as undefined and /api/memory/extract
fell back to the hardcoded default (gpt-4o-mini) for OpenAI-compatible
BYOK endpoints like MiniMax. Threading config.model through ensures the
chat-selected model reaches memory extraction.

Reviewed-by: mrcfps (review comments on ProjectView.tsx:5049)

* test: add model field to BYOK payload expectations in web workspace tests

* fix(daemon): forward BYOK chatProvider to post-turn memory extraction

The daemon close handler creates memoryOptions for pickProvider() but
never forwarded the per-call BYOK provider snapshot ('byokProvider'
from the chat request body). This meant post-turn LLM memory extraction
always fell through to environment/default provider detection instead
of using the user's selected BYOK provider and endpoint.

Added 'chatProvider: byokProvider ?? null' to the memoryOptions object
so pickProvider() can run 'Same as chat' extraction against the user's
actual provider/config, matching the behavior of the pre-turn
/api/memory/extract path.

Addresses reviewer feedback on PR #5177 (fix #5162).

* fix(daemon): normalize byokProvider shape for post-turn memory extraction (#5177)

The close handler forwarded byokProvider ({ protocol, ... }) directly as
chatProvider, but memory-llm.pickProvider expects { provider, ... }
with provider being a PROVIDER_DEFAULTS key. Now we map protocol → provider
the same way the web pre-turn path does (ProjectView.tsx L5894).

* fix(daemon): support keyless BYOK endpoints in post-turn memory extraction (#5177)

Three-part fix for keyless BYOK (local vLLM / Ollama / openai-compatible servers with requiresApiKey=false):

1. server.ts normalization: thread requiresApiKey through to the memoryChatProvider snapshot so pickProvider() can distinguish keyless endpoints from missing-provider cases.

2. memory-llm pickProvider() BYOK gate: relax the apiKey guard so endpoints explicitly marked with requiresApiKey=false enter the BYOK branch instead of falling through to env/media-config fallbacks (which would route to unrelated OpenAI credentials / gpt-4o-mini).

3. memory-llm callOpenAI(): conditionally emit Authorization header only when apiKey is non-empty, preventing empty Bearer from being sent to keyless local servers that would reject it.
2026-07-15 12:20:24 +00:00
c67048516c fix(web): polish preview status action (#5696)
* fix(web): polish preview status action

* fix(web): keep preview failure hint passive

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-15 12:09:48 +00:00
167db9de20 feat(web): add preview delivery status feedback (#5557)
* feat(web): add preview delivery status feedback

* fix(web): keep delivery retry in chat

* feat(web): stage preview delivery progress

* refine(web): clarify preview delivery stages

* fix(web): preserve preview delivery feedback

* fix(web): address preview delivery review

* refine(web): keep preview delivery status passive

* Revert "refine(web): keep preview delivery status passive"

This reverts commit df5579da48.

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
2026-07-15 09:47:02 +00:00
75af2b13cf fix(branding): preserve authored seed overrides (#5613)
* fix(branding): preserve authored seed overrides

* fix(contracts): expose brand seed overrides

---------

Co-authored-by: Rasmus Lovgren <booodhi@gmail.com>
2026-07-15 09:04:54 +00:00
open-design-release-bot[bot]GitHubopen-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
a1cbe0ec57 chore(release): bump main to 0.15.1 ahead of stable 0.15.0 (#5661)
Co-authored-by: open-design-release-bot[bot] <open-design-release-bot[bot]@users.noreply.github.com>
2026-07-15 03:30:28 +00:00
open-design-crew[bot]GitHubNJUHuaMasonopen-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
4b660237c0 feat(prompts): land the slim system-prompt line as the default charter (#5603)
* test(daemon): freeze scenario x section gating matrix for composeSystemPrompt

composeSystemPrompt splices ~30 conditional sections whose gates form an
implicit matrix (session mode x design system x skill surface x memory x
execution profile). Before any prompt-slimming refactor, this golden
snapshot freezes which sections each representative scenario receives,
so a re-gating regression surfaces as a snapshot diff instead of a
silent production behavior change.

Detection is line-anchored heading markers: discovery quotes headings
like "## Active plugin" mid-sentence, so bare substring matching would
false-positive on sections that were never spliced in. A companion
assertion fails when a marker goes stale (no scenario produces it), and
position invariants pin injection-resistance first / role-marker guard
last across every scenario.

* perf(prompts): stop shipping rare-path media/deck payloads on every run

Langfuse telemetry (24h, 2k-trace sample) puts the composed charter at
p50 96KB (~24K tokens) with a 21% stable-cache hit rate — most turns
resend it whole. Two always-on blocks serve paths most runs never take:

- MEDIA_DISPATCH_HINT (2.8KB into every non-media project) embedded a
  40-line worked bash polling loop. The loop moves to 'od media help'
  (the CLI already prints the exact next wait command on handoff), and
  the hint keeps only the dispatch rules agents historically fumbled:
  no provider REST calls, no API-key asks, POSIX not PowerShell,
  python3 not jq, fal-ai/* passthrough. 2,771 -> 1,547 chars.

- The freeform maybe-deck branch injected the full 20.7KB deck
  framework into every kind=other / no-metadata run 'just in case'.
  New detectDeckIntentSignal() scans the same request text the agent
  will see (transcript included, so a deck mention anywhere in the
  conversation stays sticky) for generous EN/CJK deck vocabulary and
  skips the framework when nothing matches: 78.5K -> 57.7K chars on
  non-deck freeform runs. False positives fall back to today's
  behavior; deck-kind projects stay unconditional. An absent signal
  (contracts/BYOK composer, tests) preserves legacy always-inject.

Also relieves the POSIX argv budget: DS-active runs sat within 4KB of
the 100KB hard cap that fails argv-bound adapters (aider/deepseek).

Baseline check: tests/media/policy-routes.test.ts (7) and
tests/connection-test.test.ts (1, local Kimi CLI version) fail
identically on the unmodified tree — no new failures from this change.

* test(daemon): refresh golden matrix snapshot for upstream memory prompt hardening

Rebase onto main picked up #4913, which reworded the memory
rule-proposal block (+230 chars in memory-bearing scenarios). Section
gating is unchanged — size-only refresh.

* feat(prompts): rewritten slim core charter behind OD_PROMPT_CORE=slim

The always-on doctrine layer (DISCOVERY_AND_PHILOSOPHY ~28K chars +
designer charter ~14K + three tail overrides that re-assert rules the
layered stack states in softer or contradictory forms) is rewritten as
ONE ~15.5K-char document: core-slim.ts. Same composed inputs drop from
65.7K to 36.0K chars (-45%); freeform non-deck runs from 57.7K to 28.9K.

Design rules of the rewrite:
- Every rule stated exactly once, under an explicit precedence ladder
  (user request > active skill > design system > memory > charter).
- Contradictions resolved instead of stacked: one verification budget
  (static pass + P0 checklist + craft scan + at most ONE render; the
  'two passes is normal' re-score loop is gone), one turn-1 form
  decision path (four skip conditions listed in one place, including
  the memory task-brief replacement), Tweaks reconciled (iteration
  surface on request, never demo controls in product files).
- Protocol markers are frozen API and survive verbatim: question-form
  shapes/ids, pick_direction/brand_spec/reference_match branch values,
  task-type routing options, data-od-id, EDITMODE markers, pinned
  React/Babel/Motion tags, artifact handoff shapes.
- Emphasis quota: no IMPORTANT/CRITICAL inflation; imperatives carry
  the weight.

Rollout: ComposeInput.promptCoreVariant ('classic' default | 'slim'),
daemon flips via OD_PROMPT_CORE=slim — zero behavior change until set,
A/B-able per daemon. Ask mode keeps the clarifying-questions tail (it
composes no core charter). Guards in tests/prompts/core-slim.test.ts:
a 16KB byte budget that fails any silent regrowth, frozen-marker
assertions, handoff profile switch, and classic-parity checks; matrix
snapshot gains slim scenarios.

Not yet covered (follow-ups): the packages/contracts BYOK composer
still ships only the classic stack; instruction-following A/B against
classic is the sign-off gate before changing the default.

* feat(prompts): shrink the slim core to an 8KB budget by evicting what a system prompt shouldn't carry

The 15.5K v1 slim charter still carried content that has no claim to an
always-on seat. v2 evicts it by category:

- Already owned elsewhere: the od-default task-type router form ships in
  plugins/_official/scenarios/od-default/SKILL.md and reaches the prompt
  as '## Active skill'; the charter now just defers to skill-owned
  turn-1 forms. An ownership test pins the form to the skill file.
- Conditional-path content: per-platform delivery contracts (breakpoint
  matrix, per-target files, native chrome) move to
  PLATFORM_CONTRACTS_BLOCK, injected only when the project declares a
  platform or multiple targets. Deck rules were already duplicated by
  the deck-gated directive - dropped from the charter entirely.
- Model-competence content: PDF extraction how-tos, attached-image
  explanations, JSON syntax reminders - deleted.
- Recital: the default discovery form example keeps only the brand
  question verbatim (its pick_direction/brand_spec/reference_match
  values are the one protocol-matched part); the other six questions
  become a one-line recipe. Option labels were always model-localized
  per language, so exact English menus were never stable anyway.
- React pins keep versions (the actual bug fix: 18.3.1 pairing +
  framer-motion React build) but drop the SRI hashes and script-tag
  boilerplate - reviewers should weigh the integrity-attribute loss.

Charter now renders at 8,187 bytes (budget test tightened 16KB -> 8KB).
Composed prompt for the common design path: 65.7K classic -> 29.6K slim
(-55%); freeform non-deck: 57.7K -> 21.5K (-63%).

* perf(prompts): evict the remaining always-on payloads from the slim stack

Follow-up to the 8KB charter: the composed slim prompt still carried
~13K of non-charter payload. Four evictions, all slim-only (classic
stays byte-stable for the A/B):

- Project metadata becomes a fact sheet ('facts' style). The classic
  block had grown seven doctrine rules plus a responsive contract and
  cross-platform rule - prose owned by the slim charter's Craft section
  and PLATFORM_CONTRACTS_BLOCK. facts keeps key-value fields, media
  workflow data, and two compact delivery lines (screen files, product
  depth). kind=other: 3.6K -> ~1.1K.
- Memory two-loop scaffolding compressed ~5.5K -> ~2.9K: identical
  headings and od-card shapes (web parses the card types; the daemon
  checks the verify-scorecard), repeated rationale prose cut.
- zh-CN locale override drops the worked quick-brief sample copy under
  slim - it mirrored the classic form verbatim, which slim recipes
  instead; the form contract already mandates localization. 2K -> 0.85K.
- MEDIA_DISPATCH_HINT gates on detectMediaIntentSignal (same
  transcript-sticky pattern as the deck signal): most conversations
  never mention media, and the hint returns for the rest of the session
  the moment one does.
- Direction library becomes a pull layer: slim composes a ~1.3K
  id+label index and the agent fetches the chosen direction's full
  palette/posture spec via the new 'od tools directions --id <id>'
  CLI (also --label / --json / bare list). One tool call on the
  direction-pick path, which runs at most once per project, replaces
  6.7K of always-on palette data. Classic keeps the inline library.

Composed slim sizes: freeform non-deck 21.5K -> 12.6K; design full
stack 29.6K -> 22.2K (fixture DS; media hint drops another 1.4K on
non-media conversations). Baseline classic: 57.7K / 65.7K.

* refactor(prompts): restructure the slim prompt as static-first layers for cache and hierarchy

Two structural defects in the slim composition, both flagged against
prompt best-practice guidance:

1. Heading hierarchy was inverted: the document opened with an H2
   ('## Security: prompt injection resistance') and the H1 charter
   appeared six sections later. The security section now lives INSIDE
   the charter as an H2 right after Precedence (single source moved to
   core-slim.ts; the classic stack imports it for its unchanged
   standalone-first position), so the composed document reads
   identity-first with a coherent H1 > H2 tree.

2. Ordering was cache-hostile: conditional blocks (mode override,
   locale, direction index, platform contracts) preceded the static
   charter — the exact 'conditional prefix sections' anti-pattern from
   the prompt-caching guidance, where every flag combination becomes a
   distinct prefix and nothing is shared. Slim now composes in
   variability tiers:

     static charter (identical across all runs — shared cacheable
     prefix) -> conversation-stable (mode, locale) -> project-stable
     (direction index, frames, platform, memory, instructions, DS,
     skill, metadata, MCP) -> turn-variable (maybe-deck and media-hint
     signal blocks, deferred to after MCP so a mid-conversation signal
     flip only invalidates the suffix) -> recency-pinned role guard.

   The charter's Precedence section now states that a session-mode
   directive appearing after it takes precedence, replacing the
   positional 'read first' authority the old ordering relied on.

Classic composition is byte-untouched (A/B baseline); ask mode keeps
the classic head. Charter budget 8KB -> 9.25KB (absorbed the ~1KB
security section; composed totals unchanged: freeform 12.7K,
full-stack 22.4K). New layered-ordering test pins the tier sequence
with line-anchored markers; the matrix position invariant now asserts
the per-variant head.

* fix(prompts): resolve five structure/expression defects from the charter review

Findings from the senior-review pass, each with a concrete failure mode:

1. Turn-ordinal section names misfire mid-session. 'Turn 1/2/3+' made
   the model treat 'now is not turn 1' as license to skip the fresh-
   brief flow when a NEW artifact is requested at turn 8 — and,
   conversely, an event-based fix alone would have re-asked questions
   the session already answered. Sections are now event-named ('On a
   fresh brief' / 'When the brand answer arrives' / 'Once direction is
   locked') AND a session-inheritance rule makes the mid-session case
   explicit: a fresh brief inherits the conversation's locked
   direction/brand/audience, asks only genuinely new unknowns, and
   builds directly when there are none — matching current production
   behavior with zero extra rounds or cache cost.

2. The handoff rule — the single highest-cost-if-violated output
   protocol (never emit <artifact> in filesystem runs) — was the only
   rule block without a heading. Promoted to '## Handoff'.

3. The form example read backwards: it showed the LAST question
   (brand) and described the earlier six in reverse ('preceded
   in-form by'), the only place in the prompt requiring the model to
   assemble structure in reverse. Now forward-order: output question
   shown in full, brand question shown in full (its values are the
   protocol-matched part), the middle questions listed as
   insert-between recipes.

4. Dangling reference: Craft said 'single family only for
   tech/utility' — a direction name defined only in the Direction
   index, which is absent whenever a design system is active.
   Reworded direction-independent.

5. direction-cards was dead vocabulary: listed as a legal control but
   every mention forbade it. Its one legitimate path is now named —
   only when the user explicitly asks to see direction options.

Charter budget 9.25KB -> 10KB: fix #3 cost ~0.7KB (restoring the full
output-question JSON), accepted deliberately — it also restores
few-shot density for weaker fleet models, and the review's own
conclusion was that the last byte-squeezing round had negative ROI.

* fix(prompts): identity-first head for slim ask mode

The last remaining hierarchy inversion: slim ask still opened with the
H2 security block before the H1 ask override. Ask now matches design
mode's structure — the ask override (the turn's whole charter) opens
the document and the security section reads as its first subsection.
Both blocks are static, so the swap is cache-neutral. Classic remains
byte-untouched.

* fix(prompts): repair five slim-vs-classic regressions from the second audit

Second senior review against the shipping classic stack surfaced two real
bugs and four wrongly-compressed classic behaviors in the slim variant:

- text_artifact/BYOK runs got the direction INDEX plus an instruction to
  run the `od tools directions` CLI they do not have; classic inlined the
  palettes. Slim now profile-switches: filesystem keeps the 1.3K pull
  index, text_artifact gets the full inline library back (+1.35K tokens
  on that path only; still ~68% below classic, and now under deepseek's
  30KB Windows argv cap, which classic never was).
- API_MODE_OVERRIDE's literal scope ('every LATER instruction … is
  overridden') inverted when slim moved the charter ahead of it: the
  charter's TodoWrite/render-check instructions escaped the override.
  Plain-stream slim runs now compose the override first again. Pure
  reorder, zero bytes.
- PLATFORM_CONTRACTS_BLOCK gated only on project metadata, missing
  freeform briefs that name a platform in text ('做个 iOS app 原型').
  New detectPlatformIntentSignal (same generous EN/CJK shape as the
  deck/media signals) ORs into the gate; server wires it.
- media-contract's 'explicit layer overrides' paragraph cross-referenced
  the classic-only layer names; reworded layer-agnostic (classic +19B).
- Restored four classic rules the compression pass misfiled as
  model-competence filler: never hot-link user-attached images by URL
  (product constraint), skill/DS per-domain precedence clause, expressive
  form-control guidance, and the modern-CSS encouragement line. Charter
  budget 10,240 -> 10,752 with rationale in the test comment.

Classic bytes untouched except the shared media-contract rewording.
Golden matrix: gating unchanged; sizes only (classic +19, slim +537).

Pre-existing failures unrelated to this change and failing on the
unmodified baseline: tests/media/policy-routes.test.ts (7), Kimi
connection test (local CLI version mismatch).

* fix(prompts): defer signal-triggered platform contracts to the turn-variable suffix

Two follow-ups from the third structure audit:

- PLATFORM_CONTRACTS_BLOCK sat in the project-stable zone while its gate
  now includes the turn-variable conversation-text signal. A mid-session
  'make it an iOS app' would insert the block early and break the cached
  prefix for every section after it (DS, skill, memory, metadata, MCP).
  The gate is now split by trigger stability: metadata triggers keep the
  early slot (fixed at project creation), signal-only triggers defer to
  slimTurnVariableParts like the deck/media signal blocks. The
  slimTurnVariableParts declaration hoists above the design section so
  the gate can reach it; no byte or ordering change for any existing
  gating combination.

- Slim ask mode on a plain stream composed CHAT -> injection -> API
  override, inverting classic's authority order (API before CHAT, so
  'overrides every rule below' covers the chat charter). The ask head
  now leads with the API override on plain streams; the later push is
  gated on !isSlimCore. Zero-byte reorder; the two overrides do not
  conflict in content, this restores parity with the shipping order.

* fix(prompts): restore form-tailoring mandate and first-message form scope

The slim-vs-classic blind A/B eval (2026-07-04, 17 pairs, double-order
opus judging) surfaced two form regressions with converging judge
rationales:

- Form quality lost 12:1:4 to classic. Mechanism: classic's discovery
  layer carries extensive per-brief form-tailoring guidance; slim
  compressed it to the single word 'tailor'. Models shipped the default
  recipe verbatim — app-platform checkboxes on landing-page briefs, the
  fundraising ask/traction buried in one textarea placeholder. Restored
  as an explicit mandate with concrete per-brief examples, a
  plausible-targets-only rule for the platform options, and a
  translate-the-labels note on the tone recipe.
- DS + detailed-brief projects skipped the turn-1 form 3/6 vs classic
  1/6: the mid-session inheritance clause generalized to first
  messages. Added the explicit scope limit: inheritance never applies
  to a project's first message.

Charter budget 10,752 -> 11,264 with the eval citation in the test
comment. Golden matrix: sizes only.

* fix(prompts): template tokens in the seed must be replaced before handoff

The 40-task x 5-turn A/B eval (2026-07-05) found slim's remaining T2
quality gap is driven by catastrophic failures, not taste: 3 of 39
pairs shipped the live-artifact skill seed with unrendered {{mustache}}
tokens (classic did this once). Mechanism: slim compressed classic's
seed-handling guidance to 'copy the seed, paste layouts', losing the
explicit fill-with-real-content step.

- Read-before-write bullet now ends with: replace EVERY template token
  with real content; a {{placeholder}} or empty section reaching the
  user is a failed delivery. (Also covers the eval's 4 thin-content
  failures.)
- Self-check static pass adds 'leftover template tokens' to its scan.

Charter 11,155B, within the existing 11,264 budget — no bump needed.

* fix(prompts): first build ships complete, no stub sections

Clean-scope T2 analysis of the 40-task eval (excluding the 10 tasks
confounded by the live-artifact skill protocol) still shows a first
build completeness gap: main 7.34 vs slim 6.10, driven by thin-content
deliveries (empty canvases, placeholder-looking blocks) on 4 of 29
pairs. The charter's 'show something visible early' line, uncushioned,
reads as license to hand off the early skeleton.

- Qualify it: '…but the turn ships the COMPLETE artifact: no stub
  sections.'
- Self-check static pass adds 'empty or stub sections' to its scan.

11,215B, within the 11,264 budget.

* refactor(prompts): split the overloaded turn-1 form section by concern

The slim charter's '## On a fresh brief' H2 bundled six distinct
concerns under one heading — trigger, default action, mid-session
inheritance, skip conditions, form-shape tailoring, and the form JSON
contract — so a maintainer could not scan the outline to find any one
rule, and the cross-cutting form contract (valid on ANY turn) was buried
under a heading scoped to fresh briefs.

Behavior-equivalent restructure (every directive's wording preserved;
only headings added and the form contract's 'any turn' note moved from a
parenthetical to the section lead):

- ## Turn 1 — the discovery form        (trigger + default action)
  - ### When to skip or inherit the form (inheritance + skip conditions)
- ## Writing a `<question-form>`         (cross-cutting: shape + tailoring + example + ordering)
  - ### Form contract                     (JSON validity + type enum + localization)

The charter outline now reads as a per-concern document. Frozen protocol
markers (form JSON, brand values, type enum, allowCustom) unchanged.
Charter 11,242 -> 11,322B; budget raised to 12,288 for readability
headroom at the maintainer's direction (budget explicitly deprioritized
for this change). Golden snapshot regenerated; 113 prompt tests green.

* refactor(prompts): readability second pass + multi-turn DS/constraint adherence

Two changes, from a critical re-read plus production feedback.

A) Readability / logic second pass (behavior-equivalent, wording preserved):
- Heading style: the two remaining imperative-tail H2s became event
  labels with the directive moved to the body lead ('## When the brand
  answer arrives', '## Once direction is locked'), so the outline reads in
  one consistent grammar. Dropping '…hand off' also fixes a heading that
  promised handoff while the rules live in the next section.
- The self-check bullet's inline (1)(2)(3)(4) run-on became a four-item
  sub-list — the least-scannable line is now the most scannable.
- The 70-word 'Read before you write' sentence split into shorter units;
  'show early / ship complete' separated into its own bullet so the two
  ideas stop reading as contradictory.
- Precedence: skill and DS collapsed into one rank split by domain (was
  numbered 2 vs 3, contradicting the 'each highest in its own domain'
  clause) — removes the only real logical seam in the ladder.

B) Multi-turn edit adherence (new '## On an edit or tweak' section):
  Production feedback reports the design system and explicit user
  constraints frequently drift during multi-turn edits. The charter was
  turn-1/first-build heavy and had no standing edit-mode invariant. Added,
  in the lifecycle between build and handoff:
  - an edit touches ONLY what the user named; untouched sections/values
    stay as they were; re-read the file and edit in place.
  - the design system binds on EVERY turn, not just first build — no
    drift, no raw hex, no palette re-pick because this turn was about
    something else.
  - every hard constraint stated this session persists across later turns
    until the user relaxes it; a new request adds to constraints, never
    silently resets them.

Charter 11,322 -> 12,029B, within the 12,288 budget (no raise). New
frozen assertion pins the adherence invariants against future
compression. 114 prompt tests + guard + typecheck green; golden snapshot
regenerated.

* refactor(prompts): two-tier charter — lifecycle spine, grouped pillars, precedence precision

From a senior word-by-word re-read. The charter was 13 flat H2s spanning
three conceptual tiers (foundations / lifecycle / reference) with no
visible spine, and the precedence wording could invert authority.

Structure (behavior-equivalent, rule wording preserved):
- 13 flat H2 -> 5 H2: two foundations (Precedence, Security) + three
  pillars, each grouping its H3s —
    ## Discovery — ask before you build   (Turn 1, skip/inherit, writing, contract)
    ## Delivery — brand → build → iterate  (brand answer, build, edit, handoff)
    ## Craft & contracts                    (craft, technical, conduct)
  The delivery lifecycle is now one connected, ordered spine — 'Editing
  an existing artifact' reads as a named phase of it, which should help
  the multi-turn drift the edit-adherence rules target.

Precision fixes:
- Precedence: 'On conflict, higher wins: 1…2…3…4' was ambiguous (which
  number is higher?) — reworded to an explicit list, 'the one higher on
  this list wins — the user's request is the highest authority, this
  charter the lowest', and the trailing pronoun tightened to 'overrides
  it wherever the two conflict'.
- 'no tool calls or file reads first' -> 'before it'; 'Resolve the
  source' -> 'Resolve the brand source'; 'restyle the whole thing' ->
  'restyle beyond the request'.
- Trimmed one of three localization restatements (kept the authoritative
  one in Form contract, now naming labels/options/placeholders) and the
  'however complete the brief reads' duplicate.

Handoff/Craft/Technical/Conduct headings demote to H3 under their
pillars (local constants, not shared with classic). Charter 12,029 ->
12,294B; budget 12,288 -> 13,312. Ordering/adherence test anchors and
golden snapshot updated. 114 tests + guard + typecheck green.

* fix(prompts): make edit-adherence forceful — do exactly what was asked, verify it landed

Production feedback and maintainer review: the edit section addressed
only scope (don't over-change) and persistence (DS/constraints), but not
the core instruction-following failure — 'the user asked for A and the
result isn't A'. The whole section was defensive; the positive imperative
that the requested change must actually happen, completely, was missing.

Strengthened '### Editing an existing artifact':
- Leads with the imperative: the delivered file must actually be A — do
  exactly what was asked, in full, in every place it applies, with
  complete-vs-partial examples ('dark green' = every element, not one).
- Forbids reinterpreting, 'improving on', partially applying, or
  substituting own taste for the user's literal words — ties to
  Precedence #1 (user's request is highest authority).
- New 'Verify before you hand off' bullet: open the file, confirm the
  change is actually present and complete and no in-force constraint
  broke; never report a change you did not make (the claim-vs-reality
  failure).
- Kept the scope / DS-binding / constraint-persistence bullets.

Charter 12,294 -> 13,122B, within the 13,312 budget. Frozen assertion
updated to pin the positive imperative and the verify bullet.

* fix(prompts): a later explicit request overrides a conflicting earlier constraint

Edge case raised in review: turn 2 'fix the color to blue, don't change
it', turn 4 'change everything to yellow'. Correct behavior is turn 4
wins (the user is changing their own mind; yellow becomes the new
standing constraint), which Precedence #1 + 'persist until the user
relaxes it' already imply — but the old bullet also said 'a new request
ADDS to those constraints', which a literal model could read as
additive-only and stall on the turn-2 'don't touch it'.

Rewrote the constraint-persistence bullet to make the override explicit:
constraints carry forward, only the user can lift or change one, and a
later explicit request overrides a conflicting earlier constraint — with
the blue->yellow turns as the in-prompt example. The anti-silent-drop
guard (model must never drop a standing constraint on its own) is kept.

Charter 13,122 -> 13,412B; budget 13,312 -> 14,336.

* fix(prompts): make the plan step agent-agnostic — TodoWrite is Claude-only

Open Design drives many local code agents; the real session mix is ~65%
non-Claude (codex, opencode, gemini, ACP family, …). The slim charter's
plan step hardcoded 'first tool call is TodoWrite' + in_progress/completed
— a Claude Code tool those agents don't have. For ~2/3 of traffic that
instruction is dead: the agent either skips planning (losing the exact
lever that helps multi-turn adherence) or hallucinates a tool it lacks.

Everything else in the slim charter was already agent-agnostic (render
checks and direction pulls go through the shell OD CLI wrapper; protocol
markup is assistant text), so TodoWrite was the one hardcoding.

- Charter plan step → generic: 'lay out a short, updatable plan; if your
  runtime has a structured plan/todo/task tool, use it, else a numbered
  list; never call a tool you don't have.'
- Composer injects a concrete note ('Your plan tool is TodoWrite; the
  host renders it as a live Todos card; mark in_progress/completed') only
  for Claude-family runs (streamFormat claude-stream-json: claude,
  codebuddy, amp) — the only agents with TodoWrite and the host's live
  Todos card. Agent-specifics live in the composer, portable doctrine in
  the charter.
- API_MODE_OVERRIDE reframed from 'TodoWrite/Read/… are unavailable' to
  'any tool call — TodoWrite/…/or whatever your runtime exposes — will
  not execute'. Keeps the specific names (issue #313 anti-pseudo-tool,
  test-pinned) while covering non-Claude runtimes.

Two frozen tests pin: charter has no TodoWrite + generic wording;
Claude-family gets the note, json-event-stream doesn't. 116 tests +
guard + typecheck green. Charter 13412 to 13543B (budget 14336).

* feat(prompts,live-artifacts): render one-level repeats, tighten slim charter craft guards

- live-artifacts/render: implement the documented `data-od-repeat="item in data.path"`
  one-level repeat directive (specified in the schema but never rendered), with
  loop-scoped `{{item.*}}` bindings, single-pass expansion so data values are never
  re-interpolated, and explicit rejection of nested repeats / non-array sources.
- live-artifact SKILL.md: state the html_template_v1 binding contract in the workflow
  (flat data.* scalars + data-od-repeat lists; no hand-loops, no bare loop vars) so
  templates stay within the renderer's capability.
- core-slim charter: "replace every template token" now yields to a skill's
  template-plus-data protocol; add an objective Layout-integrity craft rule (no
  overlap, no clipping, oversized display type must fit its column, charts use fills
  not outlines) and name those failure modes in the self-check.

* feat(prompts): add agent-agnostic production-value craft rule to slim charter

Visual-first pages regressed on slim: hero images were hand-drawn CSS boxes
instead of generated art (P1), and controls read flat/greyscale (P5). The slim
charter had dropped all of main's imagery + visual-system guidance.

Add one "Production value — feel shipped, not greyscale" craft rule: generate a
real image via the Open Design media tool (referenced through the injected
`"$OD_NODE_BIN" "$OD_BIN"` wrapper, so it is runtime-agnostic — not tied to any
one code agent) when a hero/product shot would carry the page; ship a real
product palette, colored interaction states, and elevated primary controls.

Bump SLIM_CORE_BYTE_BUDGET 14_336 -> 15_360 to seat this load-bearing guidance.

* feat(prompts): generalize the production-value imagery rule beyond hero shots

40-task eval + user review (C3 tea gift-box, P1) showed slim under-generates real
imagery on visual-first work, defaulting to hand-drawn schematic graphics. Reframe
the rule from a narrow "hero/product shot" trigger to a general default: whenever a
real picture would lift the artifact (product, place, food, person, hero, texture),
generate it via the media tool; drop to a diagram/UI mock only when that genuinely
serves the content better. Still agent-agnostic (media tool via the injected
"$OD_NODE_BIN" "$OD_BIN" wrapper).

* fix(prompts): let the production-value rule use the run's own image generation

Hard-pointing imagery at `od media generate` only works when a daemon spawned the
agent (it injects OD_PROJECT_ID); a standalone agent (eval harness, external
runtime) can't reach it, so slim silently fell back to hand-drawn placeholders
while codex-run peers used their native imagegen. Broaden the rule: generate through
whatever image generation the run offers — the Open Design media tool when wired up,
otherwise the runtime's own native image generation. Verified: C3 slim now emits a
real generated product photo instead of a schematic box.

* test(daemon): drop connected-external-mcp from the golden matrix

Upstream moved the connected-external-MCP directive out of
composeSystemPrompt into the per-turn slice (live OAuth token state must
stay out of the cached stable prefix), so it is no longer a compose
input or a gateable section.

* chore(prompts): default to the slim charter on the validation branch

VALIDATION-ONLY commit for feat/system-prompt: packaged beta builds must
exercise the rewritten slim charter, and the packaged sidecar env
allowlist does not forward OD_PROMPT_CORE, so the default flips to slim
here. OD_PROMPT_CORE=classic restores the classic stack. Do not carry
this default into main.

* perf(prompts): batch slim tool work, with quality guards restored (#5555)

* perf(prompts): batch slim tool work

* fix(prompts): restore seed-copy rule and unconditional own-browser ban

The tool-economy pass (02923153a) dropped two quality instructions as
collateral while compressing the read/preview bullets:

- 'Copy the seed and paste its layouts — don't write CSS from scratch'
  guards the visual floor when a skill ships a seed template.
- The hard 'never your own browser' ban was softened to 'no browser
  probes first', which let the candidate arm reach for Playwright after
  an export failure (2026-07-13 slim-tool-economy eval, v1_001 turn 3).

Restore both inside the batched tool-economy wording, raise the charter
byte budget 15_360 -> 15_616 following the existing restore-precedent,
and freeze both lines as regression markers.

---------

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

* feat(prompts): prefill question forms with a recommended default per question (#5556)

Every <question-form> the agent emits now ships a brief-inferred
recommended `default` on each question — an option value for
radio/select, an array of values for checkbox, concrete text for
free-text fields — so the user can review and submit the form unchanged
instead of answering from scratch. Applied across the slim charter form
contract, the full discovery rules, and the od-default task-type router
(whose options stay verbatim; prefill is the one permitted tailoring).
Both prompt mirrors (daemon + contracts) updated in sync, with the
example forms anchoring the pattern via "default": "pick_direction".

The web renderer already honoured defaults, with one gap: the `default`
alias only accepted scalars, so a checkbox array default was silently
dropped (parseDefaultValue). Fixed alongside, with parser coverage for
the radio/checkbox/text shapes the prompt now instructs.

Charter byte budget raised 15_616 -> 15_872 for the new bullet +
example anchors; prefill markers frozen in core-slim and
discovery-form tests.

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

* fix(prompts): make form prefill survive streaming key order; add web search/fetch imagery fallback (#5558)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

---------

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

* chore(prompts): land #5573 form refinements on the system-prompt validation line (#5575)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

* feat(forms): Other-chip escape hatch, 5-question cap, form-language copy, and streamed-default truncation fix (#5573)

* feat(forms): host-owned Other chip, 5-question hard cap, and form-language host strings

Three form-UX upgrades that shipped together on the validation branch:

1. Host-owned "Other" escape hatch. Finite-choice questions no longer show
   an always-on "Custom answer" input; the host injects a localized,
   dash-bordered "Other" chip (an "Other…" entry for selects) that expands
   into the type-in field via the shared accordion motion. Opening it on a
   single-choice question deselects the fixed options; picking a fixed
   option collapses a still-empty field; checkbox custom entries coexist.
   Prompts (slim charter + discovery mirrors) now ban model-authored
   catch-all "Other …" options — they would duplicate the host's — and the
   od-default router's taskType question pins allowCustom: false in all
   three copies since its own "Other" option IS the route.

2. Hard cap: 5 questions per form. The old "keep ≤7" guidance let the
   model pad forms to the example's length. The rule is now an explicit
   count-then-cut step ("count your draft; delete the least build-critical
   until 5 remain"), and the discovery example itself was trimmed from 7
   to 5 questions so the anchor matches the cap.

3. Copy + language. Form copy anchors lead with the benefit ("Prefilled
   for you — send as is, or tweak anything first"); the localization rule
   demands native phrasing with a concrete wrong-vs-right anchor (快速确认,
   not the literal 快速简报); and forms now declare a top-level "lang"
   BCP-47 tag that the web card uses to render its own strings (Other
   chip, custom-answer copy, submit CTA) in the form's language instead of
   the UI locale — no more English "Other" chips inside a Chinese form.
   New i18n key qf.otherOption ships in all 19 locales.

Charter byte budget 15_872 → 16_896 across the two documented bumps, with
frozen-marker tests pinning the escape-hatch delegation, the hard cap, the
native-localization rule, and the lang anchor.

* fix(web): never surface a mid-stream question-form default

The streaming shaper reveals a question the moment its label parses, and
runs every question object — including the trailing, still-open one —
through mapRawQuestion. That let a MID-STREAM `default` reach the card:
the partial-JSON repair terminates it early, so "单位教育" surfaces as
"单位教" and ["历史背景与经过", "抗战精神与当代意义"] as ["历史背景与经过",
"抗战精"]. The card adopts a streamed default only while the answer is
still empty, so a truncated adoption freezes garbage that the completed
value can never overwrite — checkbox defaults (long arrays, near-certain
to be cut across delta chunks) effectively never prefilled, while short
radio scalars usually survived inside one chunk. Observed in production
run beaf2da0 (抗日主题汇报PPT): both maxSelections checkboxes rendered
with nothing selected despite the model emitting complete defaults.

Fix at the source: the trailing in-flight question exposes NO
defaultValue until its braces close. A closed object's braces are
balanced, so its default is complete; the card's empty-and-untouched
adoption rule then fills it exactly once, correctly.

Red spec reproduces the production body cut mid-default (went red on the
pre-fix tree) and pins that a closed question's default surfaces while
the next in-flight question's stays hidden.

* fix(prompts): align task-type form contract across all three copies; fit the slot menu inside the 5-question cap

Review follow-ups from PR #5573 (nettee):

- The od-default skill's task-type form now carries the same top-level
  "lang" tag as the daemon/contracts prompt copies, so host-rendered
  controls on that path key off the form language instead of falling back
  to the app locale. A new parity check reads all three copies and locks
  both contract keys ("lang" + taskType allowCustom: false) so the skill
  copy cannot drift again.

- The slim charter's default-shape recipe contradicted the new hard cap:
  "Between output and brand, in this order: platform, audience, tone.
  After brand: scale, then constraints" prescribed 7 questions in the
  same breath as "Hard cap: 5". The recipe is now 2 fixed slots plus a
  pick-AT-MOST-3 menu (same option vocabulary, same order), and the
  frozen-marker test asserts the menu wording is present while the old
  prescriptive sequence is gone — the charter can no longer carry both.

---------

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

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>

* chore(prompts): land #5586 photo-overlay discipline on the system-prompt validation line (#5600)

* fix(web): adopt streamed question-form defaults that arrive after the question renders

The #5556 prefill contract asks the model to ship a recommended `default`
on every <question-form> question, but whether the prefill actually showed
up depended on JSON key order: the partial-JSON parser reveals a question
as soon as its label lands, and a `default` emitted after `options`
(observed in production run fca86faa) arrived only after the reveal pass
had already parked the question on its auto-assigned empty value — which
the backfill effect then treated as "answered" and never touched again.

Fix on two sides:

- Web: QuestionFormView now tracks which questions the user actually
  touched (seeded from submitted/draft snapshots) and adopts a
  late-arriving streamed default via shouldAdoptStreamedDefault — only
  while the question is untouched AND still holds its auto-assigned empty
  value, so a late default can never clobber a real answer or an
  intentional clear (checking then unchecking a box stays cleared).
- Prompts (daemon + contracts mirrors, slim charter + full discovery
  rules): instruct models to write `default` before `options`, as the
  example forms already anchor, so the recommendation streams in with the
  question instead of trailing a long options array.

Red spec: "adopts a default that streams in after the question was
revealed" went red on the pre-fix tree and green with this change; a
companion guard pins that "untouched" means never-interacted, not
currently-empty.

* feat(prompts): fall back to web search/fetch imagery when image generation is unavailable

The production-value craft rule tells runs to reach for real imagery
through the OD media tool or the runtime's native image generation — but
a run with neither (BYOK agents without media config, runtimes without
imagegen) had no sanctioned next step and shipped the exact fallbacks the
rule bans: empty slots, schematic boxes, flat icons.

The slim charter's imagery guidance is now an explicit resolution chain:
OD media tool → native image generation → web search / web fetch. The new
final step has the run find a fitting real photo, pull it into the
project (e.g. curl), and reference it by relative path — never hot-link
the remote URL, matching the existing no-hot-link file rule so artifacts
don't rot with dead image links.

Charter byte budget raised 15_872 -> 16_128 for the fallback sentence,
documented in the budget history; a new frozen-marker test pins the three
chain steps and the no-hot-link wording so a future tool-economy pass
can't silently drop them.

* feat(forms): Other-chip escape hatch, 5-question cap, form-language copy, and streamed-default truncation fix (#5573)

* feat(forms): host-owned Other chip, 5-question hard cap, and form-language host strings

Three form-UX upgrades that shipped together on the validation branch:

1. Host-owned "Other" escape hatch. Finite-choice questions no longer show
   an always-on "Custom answer" input; the host injects a localized,
   dash-bordered "Other" chip (an "Other…" entry for selects) that expands
   into the type-in field via the shared accordion motion. Opening it on a
   single-choice question deselects the fixed options; picking a fixed
   option collapses a still-empty field; checkbox custom entries coexist.
   Prompts (slim charter + discovery mirrors) now ban model-authored
   catch-all "Other …" options — they would duplicate the host's — and the
   od-default router's taskType question pins allowCustom: false in all
   three copies since its own "Other" option IS the route.

2. Hard cap: 5 questions per form. The old "keep ≤7" guidance let the
   model pad forms to the example's length. The rule is now an explicit
   count-then-cut step ("count your draft; delete the least build-critical
   until 5 remain"), and the discovery example itself was trimmed from 7
   to 5 questions so the anchor matches the cap.

3. Copy + language. Form copy anchors lead with the benefit ("Prefilled
   for you — send as is, or tweak anything first"); the localization rule
   demands native phrasing with a concrete wrong-vs-right anchor (快速确认,
   not the literal 快速简报); and forms now declare a top-level "lang"
   BCP-47 tag that the web card uses to render its own strings (Other
   chip, custom-answer copy, submit CTA) in the form's language instead of
   the UI locale — no more English "Other" chips inside a Chinese form.
   New i18n key qf.otherOption ships in all 19 locales.

Charter byte budget 15_872 → 16_896 across the two documented bumps, with
frozen-marker tests pinning the escape-hatch delegation, the hard cap, the
native-localization rule, and the lang anchor.

* fix(web): never surface a mid-stream question-form default

The streaming shaper reveals a question the moment its label parses, and
runs every question object — including the trailing, still-open one —
through mapRawQuestion. That let a MID-STREAM `default` reach the card:
the partial-JSON repair terminates it early, so "单位教育" surfaces as
"单位教" and ["历史背景与经过", "抗战精神与当代意义"] as ["历史背景与经过",
"抗战精"]. The card adopts a streamed default only while the answer is
still empty, so a truncated adoption freezes garbage that the completed
value can never overwrite — checkbox defaults (long arrays, near-certain
to be cut across delta chunks) effectively never prefilled, while short
radio scalars usually survived inside one chunk. Observed in production
run beaf2da0 (抗日主题汇报PPT): both maxSelections checkboxes rendered
with nothing selected despite the model emitting complete defaults.

Fix at the source: the trailing in-flight question exposes NO
defaultValue until its braces close. A closed object's braces are
balanced, so its default is complete; the card's empty-and-untouched
adoption rule then fills it exactly once, correctly.

Red spec reproduces the production body cut mid-default (went red on the
pre-fix tree) and pins that a closed question's default surfaces while
the next in-flight question's stays hidden.

* fix(prompts): align task-type form contract across all three copies; fit the slot menu inside the 5-question cap

Review follow-ups from PR #5573 (nettee):

- The od-default skill's task-type form now carries the same top-level
  "lang" tag as the daemon/contracts prompt copies, so host-rendered
  controls on that path key off the form language instead of falling back
  to the app locale. A new parity check reads all three copies and locks
  both contract keys ("lang" + taskType allowCustom: false) so the skill
  copy cannot drift again.

- The slim charter's default-shape recipe contradicted the new hard cap:
  "Between output and brand, in this order: platform, audience, tone.
  After brand: scale, then constraints" prescribed 7 questions in the
  same breath as "Hard cap: 5". The recipe is now 2 fixed slots plus a
  pick-AT-MOST-3 menu (same option vocabulary, same order), and the
  frozen-marker test asserts the menu wording is present while the old
  prescriptive sequence is gone — the charter can no longer carry both.

---------

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

* feat(prompts): pin photo-overlay placement discipline in the slim charter

Real-imagery production value (the strongest-texture-lever rule) kept
shipping badges and caption cards that straddle the hero image's edge or
sit on the photo's focal subject — 2026-07-14 beta feedback on a
campus-open-day landing hero showed a session-type chip half-off the
photo, covering the graduates.

New Craft bullet: an overlay on a photo pins to ONE corner with a
consistent inset, sits fully inside the image bounds, stays clear of
faces/subjects, and reads on a legible surface with separating shadow;
with no safe corner it moves beside the image. Byte budget
16_896 -> 17_408 per the in-file bump trail; markers frozen.

* chore: retrigger ci

UI P0 (entry-settings) failed on 'Use with query carries the hydrated
starter prompt' — orthogonal to this PR's prompt-text diff, green on the
base branch's identical run, and passes locally on this branch (35s).
Empty commit to re-run the shard.

* chore: drop local-build churn in next-env.d.ts

A local 'next build' rewrote the generated routes.d.ts reference path and
the merge commit swept it in via add -A; restore the mainline version —
this PR's intent touches prompts/tests only.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>

* fix: address review findings — repeat-directive false positives, directions flag validation, router-form cap exemption

1. live-artifacts/render.ts: a data-od-repeat match now counts as a
   directive only inside an element's open tag; literal mentions in prose
   or comments render as inert content per html_template_v1 (red spec in
   tests/live-artifacts/render.test.ts).
2. cli.ts tools directions: --id/--label now fail fast on a missing
   value, a flag posing as the value, duplicates, or both flags at once
   (red spec in tests/directions-cli.test.ts) — agents call this straight
   from the prompt contract.
3. prompts/discovery.ts (+contracts mirror): the 5-question hard cap now
   names the verbatim six-field task-type router form as its one
   sanctioned exception, resolving the contradiction between two locked
   instructions; the cap itself is unchanged for authored/tailored forms.
   Marker frozen in discovery-form.test.ts; matrix snapshot regenerated.

* fix: second-round review — nested literal repeat false positive, keyboard-accessible Other chip

1. live-artifacts/render.ts: the nested-repeat guard now uses the same
   inside-open-tag detection as the top-level scan (extracted
   findRepeatDirective helper), so a literal data-od-repeat mention inside
   a repeated element's body renders inert instead of throwing; a REAL
   nested directive still throws. Red spec: reviewer's exact repro added,
   red on the previous head.
2. web QuestionForm Other chip: replaced the display:none checkbox-in-label
   with a real <button aria-pressed aria-expanded> plus focus-visible ring
   and UA-chrome reset, restoring Tab reachability for keyboard and
   screen-reader users. Existing chip tests migrated to the button role;
   new focus/toggle spec added.

* chore: retrigger ci

Web workspace tests failed on HomeView.community-filter-decouple
(aria-selected timing) — a main-side test untouched by this PR; the full
web suite passes locally on this exact head (4351/4351). Empty commit to
re-run the shard.

* fix: third-round review — media runs skip the slim design charter; token-aware repeat-directive scan

1. prompts/system.ts: resolve the media surface (image/video/audio via
   skillMode or metadata.kind) BEFORE building the prompt head, and gate
   the slim design charter on it — the media-generation contract is again
   the sole workflow authority on media runs, matching classic. The
   mid-conversation clarifying-questions block now keys off the charter's
   actual presence so media runs keep classic's clarify guidance. New spec
   proves image/video/audio slim prompts contain no discovery-form or
   HTML-handoff rules while design runs keep the charter.
2. live-artifacts/render.ts: findRepeatDirective is now quote- and
   comment-aware — a directive-looking string inside another attribute's
   quoted value or inside an HTML comment is authored text. Reviewer
   repros added as regressions (attribute-quoted literal, tag-like
   comment text); nested detection shares the same scanner.
Red-checked: 3 new specs fail with the two source files reverted.

* fix: fourth-round review — slim media head drops the Ask charter; comment-aware element-boundary scan

1. prompts/system.ts: the non-charter slim fallback head applied
   CHAT_MODE_OVERRIDE to every slim run that was not the design charter —
   including media runs, whose Ask charter ('do not create images/video/
   audio') contradicted the media contract. The head now branches
   explicitly: ask -> Ask charter, slim media (non-ask) -> classic skeleton
   (plain-stream API override + injection resistance) with the media
   contract as sole authority. Media spec extended to assert '# Ask mode'
   is absent.
2. live-artifacts/render.ts: extracted insideComment() and applied it to
   findElementEnd(), so same-tag tokens inside comments no longer move the
   depth counter (<!-- <div> --> threw 'unbalanced'; <!-- </div> --> closed
   the repeat early). findRepeatDirective shares the helper. Reviewer
   repros frozen as regressions.
Red-checked: both new specs fail with the two source files reverted.

---------

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: Mason <jinmeihong0201@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
2026-07-14 10:21:32 +00:00
d7151b2b60 feat(analytics): link downloads and first-party browser opens (#5442)
* feat(analytics): attribute desktop downloads

* fix(analytics): initialize attribution routes after HTTP deps

* feat(analytics): bridge first-party browser opens

* feat(analytics): bridge first-party anchor opens

* fix(analytics): make attribution claims idempotent

* fix(analytics): record pending attribution results

* chore(analytics): bind landing attribution KV

* fix(ci): remove landing preview wrangler config override

* fix(landing): resolve download attribution function

* fix(analytics): harden attribution token consumption

* fix(analytics): preserve same-installation claim retries

* fix(analytics): merge recovered attribution retries

* fix(analytics): preserve attributed browser links

* fix(analytics): clear attribution on privacy reset

* fix(analytics): bridge stopped AMR console links

* fix(ci): deploy attribution schema before Pages

* fix(ci): run attribution migrations with Wrangler 4

---------

Co-authored-by: Siri-Ray <2667192167@qq.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
2026-07-14 08:46:04 +00:00
lefarcenandGitHub da7c4ae051 fix(web): rank Community "Newest" by shipped publishedAt, not local install timestamps (#5597)
* fix(web): rank Community "Newest" by shipped publishedAt, not local install timestamps

The Community gallery Newest toggle sorted by the installed-record
updatedAt/installedAt. Those are stamped when a machine seeds its own
database, so a fresh install (or an upgrade that re-stamps the catalog)
writes the whole bundled catalog milliseconds apart in folder-walk
order — Newest showed reverse walk order (roughly reverse-alphabetical),
unrelated to publication recency.

Root-cause fix: bundled templates now ship a real publication date.

- packages/contracts: add optional manifest field publishedAt (ISO 8601).
- apps/web sortByNewest: freshness = publishedAt when parseable, else
  updatedAt (right signal for user-installed plugins); tie-breaks and
  stability unchanged.
- plugins/_official: backfill publishedAt into all 460 bundled manifests
  from each folder's first-appearance commit date in upstream git
  history.
- e2e: repository-resource guard requiring every bundled manifest to
  declare a parseable publishedAt, so the signal cannot silently erode.
- docs: field reference entries in plugins-spec (en + zh-CN).

Issue: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/ca0778af-97f5-4fc0-bc63-739280904979

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* worker: 模板用最新筛选,筛选出来的不是最新的

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): keep shipped appeal order inside same-publishedAt batches

Review follow-up (#5597, nettee): after the freshness tie on an equal
publishedAt, the comparator still consulted local updatedAt/installedAt,
which a fresh install stamps in folder-walk order — so every
same-publication batch (largest: 322 manifests on one catalog date)
re-sorted per machine instead of keeping the shipped visual-appeal
ranking. Same-publishedAt records now preserve their incoming position;
local timestamps break ties only when at least one side has no usable
publishedAt. Adds the requested regression spec pinning
same-publishedAt / different-updatedAt records.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

* fix(web): gate publishedAt freshness to bundled records; untrack oversized looper attachment

Review follow-up (#5597, nettee round 2): publishedAt was overriding
updatedAt for every visible plugin, so a user-installed github/local/
marketplace plugin carrying an old manifest publishedAt would be buried
behind bundled templates despite a newer local install. publishedAtMs
now returns null for non-bundled records — "Newest" means install
recency for user installs, catalog publication for bundled ones. Adds
the requested regression (non-bundled, old publishedAt, newer
updatedAt) and clarifies the field docs (en + zh-CN).

CI repair: commit 578f7a9 tracked .looper-attachments/bug13_1.png
(3.8MB bug screenshot), tripping the Static gate 1MiB blob guard and
the Validate workspace aggregate. Untrack it and gitignore
.looper-attachments/ so harness attachments stay out of the repo.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 08:43:57 +00:00
9d055a0fa6 feat(amr): add artifact-based upgrade guidance for Free users (#5591)
* feat(amr): add artifact-based upgrade guidance

* fix(amr): fail open when billing plan is unavailable

---------

Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
2026-07-14 08:11:07 +00:00
lefarcenandGitHub ebada4cac3 fix(daemon): pin mermaid dark-theme discipline into the deck framework prompt (#5595)
Dark-themed decks and pages that embed a Mermaid diagram had zero guidance
on diagram theming, so the model shipped Mermaid's default (light) theme on
dark slides: near-black edge labels, black strokes, and a transparent svg
background sitting directly on the dark slide background. Reproduced on a
dark deck slide (--bg #101014): edge labels measured ~3.3:1 contrast (below
WCAG AA) and connector lines were nearly invisible (~1.9:1) — matching the
0.14.1 acceptance report "暗黑模式下 mermaid 图表的文字与背景对比度不足".

Add a compact "Mermaid diagram theme discipline" section to
DECK_FRAMEWORK_DIRECTIVE, sibling to the data-chart discipline (#5542):
prefer deck-token-styled HTML/CSS/SVG diagrams; when embedding Mermaid,
derive the theme from the slide background at initialize time (dark
background → theme: 'dark', or theme: 'base' + themeVariables with a dark
primaryColor — darkMode alone keeps the cream node fill default); never
rely on the transparent svg backdrop; plus a drift-mode ban list and a
pre-handoff/pre-emit self-check item. Mirror the identical section into the
contracts copy so API/BYOK mode is covered too, keeping each side's
self-check heading intact.

With the disciplined init, the same dark slide renders the diagram at
≥7.9:1 (edge labels) and 10.5:1 (node text) contrast.

Anchor the change with prompt-composition tests on both sides asserting the
directive carries the mermaid-theme markers (red on main, green here).

Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/9cd2904d-e52c-4813-9e4f-e7a3512aaa12

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-14 06:44:01 +00:00
lefarcenandGitHub 83d67d5a62 fix(packaged): widen win32 daemon status-wait budget + back off polling (#5515)
* fix(packaged): widen win32 daemon status-wait budget + back off polling

Windows first launches are the single biggest packaged startup-crash
bucket (packaged_runtime_failed, failure_kind=unknown): ~178 events / 61
devices/day, all status-wait timeouts with no exit code, signal, or
daemon log. PostHog shows ~90% of those devices opened the app on a
later launch — the daemon was slow, not dead — because Defender scanning
freshly-written packaged binaries inflates the cold start (native
better-sqlite3 load + first SQLite open/migrate + status-pipe bind) past
the 35s wait budget.

- resolveDaemonStatusTimeoutMs is now platform-aware: win32 gets 90s of
  AV-scan headroom, other OSes keep 35s. The OD_LEGACY_DATA_DIR
  migration override still wins on every platform. The web sidecar wait
  uses the same widened baseline (it has no legacy path).
- waitForStatus backs off geometrically (150ms -> 1.5s cap) between
  connect polls instead of a flat 150ms, so a not-yet-bound pipe is not
  hammered with connects that add contention to an already-slow start.
- classifyStartupFailure tags the timeout message as `status-timeout`
  instead of `unknown`, so the bucket is measurable — the signal for
  whether the budget raise actually drained the AV-scan-slow launches.

* fix(packaged): keep timeoutMs a hard bound — cap the backoff sleep to remaining budget

Review follow-up (@nettee): with the new geometric backoff, waitForStatus could
overshoot the configured status-wait budget by a full backoff interval — a
slow/dead sidecar could sleep the whole 1.5s cap after the deadline had passed
before the while-condition re-checked it (~2.3s over, vs ~0.95s pre-change).
Since this PR intentionally widens the win32 budget, the overshoot matters.

Cap each poll sleep to the time remaining before the deadline (and break out
immediately when none remains), so timeoutMs stays a hard-ish upper bound; the
only residual overshoot is the single in-flight requestJsonIpc connect timeout.
2026-07-13 14:30:35 +00:00
lefarcenandGitHub bdc66c978e fix(daemon): pin data-chart discipline into the deck framework prompt (#5542)
Chat-generated decks that fall through to DECK_FRAMEWORK_DIRECTIVE (no-skill-seed
deck projects + the freeform slide-deck path) had zero guidance for data charts,
so the model hand-wrote div/CSS bar charts with two stable failure modes that
match the reported symptoms: bar lengths eyeballed as magic `height: NN%` numbers
that don't track the data, and value labels clipped away inside fixed-height bars.

Add a compact "Data chart discipline" section to the directive: a paste-ready
skeleton that carries each value as an inline `--v`, declares one shared `--max`
on the chart container, and computes bar length via
`calc(var(--v) / var(--max) * 100%)`; a rule that every data point renders a
visible category + value label outside the bar (never inside a clipping
fixed-height track, same root cause as the brand-deck `funnel-label-clipped`
guard); a drift-mode ban list; and two pre-handoff / pre-emit self-check items.
Mirror the identical section into the contracts copy so API/BYOK mode is covered
too, keeping each side's self-check heading intact.

Anchor the change with prompt-composition tests on both sides asserting the
directive now carries the chart-discipline markers (red on main, green here).

Source: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/34ed7e1b-5676-48d6-8195-a4be71124ad4

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-13 10:29:46 +00:00
6426847fba fix(web): separate run success from result delivery (#5525)
* fix(web): distinguish run success from result delivery

Persist delivery outcomes separately so successful design runs cannot report completion before a usable artifact is available.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): recover missing design delivery after reload

* chore(ci): rerun pull request checks

* fix(web): avoid reattaching local delivery finalization

* fix(web): retry delivery failures

* chore(ci): rerun pull request checks

* chore(ci): retrigger managed checks

---------

Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 10:16:04 +00:00
lefarcenandGitHub d4762a9522 fix(desktop): break renderer crash-reload loops with a circuit breaker (#5322)
* fix(desktop): break renderer crash-reload loops with a circuit breaker

A renderer that crashes deterministically — a GPU/V8 CHECK failure, a
corrupt profile that aborts on every load — is reloaded forever by the
desktop poll loop: load → crash → reload → crash. One 0.14.0 device did
exactly this, emitting 26,011 desktop_renderer_crash events in a single
day (96% of all renderer crashes on the release) while its window stayed
blank the whole time.

Add RendererCrashLoopBreaker (pure, unit-tested): after 5 crashes inside
a 60s rolling window it opens. The poll loop then parks on a static,
dependency-free error screen that explains recovery is automatic, rather
than cycling the crash; markRendererFailed no-ops while open. Per-crash
telemetry is suppressed once open — the tripping crash is still reported,
flagged loop_tripped — so one wedged device can't emit tens of thousands
of identical events. The breaker re-arms after a 5-minute quiet cooldown
so a transient fault still self-heals without a reinstall.

* feat(desktop): make crash-loop recovery active + observable

Review feedback: the breaker only "waited" — its cooldown re-arm reloaded
the same URL from the same cache, and the whole flow was near-invisible.

On re-arm it now actively recovers: clears the HTTP + V8 code caches
(non-destructive — no cookies or storage) before the retry reload,
targeting the most common recoverable cause (a corrupt cached bundle or
shader) instead of hoping conditions changed on their own.

And it's now observable: a console.warn when the breaker opens (parking),
a console.info per recovery attempt, and a bounded recovery-attempt
analytics signal (reason:"recovery-attempt", recovery_attempt: N, at most
one per cooldown). Triage can tell a chronic looper (index keeps
climbing) from a device that recovered (events simply stop) without
re-introducing the per-crash spam the breaker was built to kill.

* revert(desktop): drop active cache-clear recovery, keep it passive + observable

Reviewer worry: clearing caches on every cooldown could amplify churn on a
wedged device without helping the dominant crash cause (a GPU/V8 CHECK,
exit 0x80000003), i.e. pour oil on the fire. Back the breaker off to
"stop the loop + passive retry": on re-arm it reloads without mutating any
device state and lets a transient fault clear on its own.

The observability added alongside stays: a warn on breaker-open, an info
per recovery attempt, and the bounded recovery-attempt analytics signal
(recovery_attempt: N) so chronic loopers are still visible.

* feat(desktop): add report + save-logs actions to the renderer crash screen

The recovery screen was informational only. On a renderer crash the daemon
is still alive, so give the wedged user an actionable exit — reusing IPC the
preload already exposes, no new surface:

- "Report a problem" → window.__od__.openExternal() opens a prefilled GitHub
  new-issue whose title + body carry the app version, OS, and the renderer
  exit code (e.g. 0x80000003) a triager needs, and ask the user to attach
  the logs.
- "Save logs…" → window.openDesignDesktop.exportDiagnostics() runs the
  existing diagnostics-bundle export (save dialog → zip).

Neither an issue URL nor mailto can carry a file attachment, so the flow is
save-then-attach. Each button hides itself if its bridge is missing (preload
failed to load) to avoid a dead control.

* feat(desktop): add "Email us" (support@open-design.ai) to the crash screen

Alongside "Report a problem" / "Save logs…", the recovery screen now offers a
prefilled mailto to our support address for users who'd rather email than open
a GitHub account. It reuses the existing openExternal bridge; the
shell:open-external handler now also allows a mailto addressed strictly to the
support address (isSupportMailtoUrl, unit-tested) — no other scheme or address
opens, so widening past http can't be abused into arbitrary-scheme launches.
The link hides itself if the bridge is absent, like the other actions.

* style(desktop): match crash screen to the app's monochrome brand palette

The crash screen used a stray blue accent that didn't match the app. Repaint
it with the neutral design tokens from apps/web (tokens.css): warm off-white
#faf9f7 + near-black #1a1916, a monochrome near-black primary button, and a
plain underlined (not blue) email link. Dark mode inverts the pair to a light
button on the dark bg (a near-black button would vanish), mirroring the
black/white onboarding look.

* fix(desktop): address review — tighten support mailto + reveal crash screen

Two Looper findings on the crash-screen actions:

- Security: isSupportMailtoUrl validated only protocol + pathname, so
  `mailto:support@open-design.ai?bcc=attacker@example.com` (or ?to=/?cc=)
  still passed and shell.openExternal would launch the mail client with
  extra recipients/headers through the renderer-exposed bridge. Now reject
  any query key other than the crash screen's own subject/body.

- Startup lifecycle: parking on the crash screen left `revealed` false and
  the splash open when the loop tripped before revealWhenReady(), so the
  runtime's show() kept focusing the splash — a user re-focusing during a
  startup crash loop was sent back to the boot splash. Route the park
  through revealMainWindow() so the recovery screen becomes the revealed,
  focused window and the splash is torn down (no-op after normal reveal).

* feat(diagnostics): make the support bundle root-cause native renderer crashes

Audit of "Save logs…" for the crash-recovery screen: on a renderer crash the
bundle collected text logs but NOT the one artifact that can explain an opaque
native abort like 0x80000003 (a V8/Chromium CHECK) — a minidump. crashReporter
was never started (no dumps existed anywhere), the crash-report collector was
macOS-only, and there was no GPU info (the usual suspect for such crashes).

Close the gap end to end:
- apps/desktop: start crashReporter (uploadToServer:false — on-device only) and
  relocate Electron's crashDumps into <logs/desktop>/crashes, inside the log
  tree the export already bundles; snapshot GPU + feature-status +
  runtime info to <logs/desktop>/gpu-info.json at startup.
- packages/diagnostics: add a `binary` LogSource kind (copied verbatim, no text
  decode/redaction so a minidump isn't corrupted) and findCrashDumps() to
  collect .dmp files; buildDiagnosticsZip gains a `crashDumps` input.
- apps/daemon: bundle logs/desktop/gpu-info.json and the relocated crash
  minidumps (cross-platform, capped, within 14 days).

The renderer crash context (reason/exit code/url) is already captured — the
main-process console.error lands in logs/desktop/latest.log — so a bundle now
carries stack (minidump) + GPU state + context: enough to root-cause.

* fix(desktop): mkdir crashDumps dir before setPath so first-crash dumps land in the log tree

Review (PerishCode): app.setPath('crashDumps', dir) throws when the dir does
not exist; on a fresh runtime logs/desktop/crashes won't exist yet, so setPath
threw, we fell back to Electron's default crashDumps location, and the daemon
export (which only scans <logs/desktop>/crashes) missed the minidump for
exactly the first crash loop this is meant to root-cause. mkdirSync the dir
(recursive) before setPath.

* fix(desktop): reject CR/LF in the support mailto subject/body

Review (PerishCode): the key allowlist rejected to/cc/bcc keys, but a decoded
CRLF in an allowed value (subject=ok%0D%0ABcc:attacker@…) could still inject a
mail header once the mailto reaches the mail client. Reject any subject/body
value containing CR or LF.
2026-07-13 04:58:16 +00:00
lefarcenandGitHub bc5b6f0588 fix(daemon): flag runs that end with unfinished work so status never reads "Completed" early (#5470)
A design-generation run that ended its turn with TodoWrite tasks still
pending / in_progress / stopped — or was truncated by max_tokens — was
stamped `succeeded` and read "Completed" on the Pet task center and the
project pill, while the chat footer simultaneously read "Stopped with
unfinished work". Users trusted "Completed", assumed the work was done, and
re-requested, creating duplicate generations and eroding trust in the
run-state UI (#1247 / #1060).

Root cause: the daemon equates "the turn/process ended" with "succeeded" and
never consults the agent's own declared work; multiple web surfaces then map
`succeeded` -> "Completed" without any completeness signal, even though the
chat footer already distinguishes unfinished work.

Fix — one canonical signal, consumed everywhere so the surfaces cannot drift:

- packages/contracts/api/run-completeness.ts defines the canonical
  "unfinished declared work" predicate (status !== 'completed', so pending /
  in_progress / stopped all count), matching the web footer's
  unfinishedTodosFromEvents exactly.
- Daemon folds the last TodoWrite snapshot + a max_tokens truncation off the
  agent event stream (every runtime, via the single emitAgentEvent choke
  point) and derives run.endedWithUnfinishedWork once at finish().
- The project-status projection derives `incomplete` from the already
  persisted events_json — no schema migration — so it survives reload/reattach
  instead of flashing "Completed" once the in-memory run ages out.
- Contracts carry endedWithUnfinishedWork on ChatRunStatusResponse and the SSE
  end payload (backward compatible: absent = finished), plus a new
  `incomplete` ProjectDisplayStatus.
- Web surfaces (Pet task center, project pill, DesignsTab) render `incomplete`
  as amber needs-attention, never the success color; the footer now sources
  the same shared predicate.

Truncation uses the display-only option (spec Option A): the run stays
`succeeded` but is flagged incomplete, avoiding the code===0 / stdin-close
hazards of reclassifying a truncated turn to failed.

Regression coverage, red on main / green here: an HTTP-boundary e2e
(e2e/tests/dialog/task-completeness.test.ts) drives the interactive chat path
for unfinished / stopped / truncated / finished runs; daemon + contracts +
web unit tests pin the projection and the canonical predicate.

Ref: https://plane.powerformer.net/open-design/projects/49832a02-3158-4faf-bf2f-d0e39c40c7e6/issues/011f6f51-7a25-4043-ae94-182e0b8b0931

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
2026-07-13 04:17:24 +00:00
9166c3bf09 feat(contracts): agent-ready capability contracts + design RFCs (#5398) (#5400)
* feat(contracts): agent-ready capability contracts + design RFCs (#5398)

Slice 1 of the "agent-ready OD" proposal (#5398): pure-TypeScript contract
shapes for the assistant's capability layer, plus the design RFCs. No behavior
code.

- packages/contracts/src/agent-tools/: descriptor (declare), actions
  (call/result), manifest (list + paged search), registry (port), task
  (cross-project AgentTask). Discriminated on `surface`; dual-track law is
  structural; action identity is a daemon-minted `invocationId`.
- docs/rfc-drafts/agent-ready.md: umbrella design + slice roadmap, the
  MCP/WebMCP-congruent capability model, tool-search options, and
  "options considered -> recommendation" for the load-bearing decisions.
- docs/rfc-drafts/agent-ready-cross-project.md: the maintainer's separate
  reviewable permissions writeup (AgentTask + permission options).

Registry runtime, endpoints, and behavior are later slices per the roadmap.

* fix(contracts): narrow AgentToolSearchQuery.surface to 'api'

Search is the PULL path over the persistent registry, which per the RFC
holds only api-surface tools — browser availability is PUSH-advertised
per-session and cannot be answered by a session-less search query. Narrow
the searchable surface to 'api' so the contract does not encode a
browser-tool discovery path the runtime would have to special-case or
break. Addresses the blocking review on #5400.

* fix(contracts): address review — narrow search result to api-only; distinct browser vs api call paths

Resolves the two remaining blocking findings on PR #5400:

- manifest.ts: narrow `AgentToolSearchResult.tools` from the full
  `AgentToolDescriptor` union to `ApiToolDescriptor[]`, enforcing the
  PULL/api-only search invariant on the response side too (the query
  `surface` was already narrowed to `'api'`). A registry impl can no
  longer type-check while leaking `browser` descriptors into a
  session-less search result.

- agent-ready.md: stop presenting `BrowserActionRequest` as the generic
  `tools/call` for every capability. The two surfaces now have distinct
  request paths: `api` tools are invoked through their declared `/api/*`
  route + `od` subcommand (no round-trip envelope), while
  `BrowserActionRequest`/`Result` is the browser-surface path only — the
  genuinely new inbound seam.

- agent-ready-cross-project.md: reconcile the slice-5 cross-project
  routing fields (`taskId?`/`targetProjectId?`) with the distinct-paths
  model — added on both the browser envelope and the api-call request,
  not bolted onto `BrowserActionRequest` for api tools.

Validation: contracts typecheck pass; pnpm guard pass.

---------

Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
2026-07-11 08:38:54 +00:00