Skip to content

v0.8.7: perf improvements, custom blocks improvements, crunchbase, pitchbook, bitbucket integrations - #6885

Merged
waleedlatif1 merged 24 commits into
mainfrom
staging
Aug 20, 2026
Merged

v0.8.7: perf improvements, custom blocks improvements, crunchbase, pitchbook, bitbucket integrations#6885
waleedlatif1 merged 24 commits into
mainfrom
staging

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

icecrasher321 and others added 22 commits August 19, 2026 15:25
…e, and map blocks per environment (#6857)

* feat(custom-blocks): join cross-workspace runs into the caller's trace, and map blocks per environment

Teams that orchestrate work across workspaces have two gaps that keep them on HTTP
blocks instead of custom blocks: they cannot see what a custom block actually did,
and a forked environment silently keeps calling the environment it was forked from.

Debugging. A custom block is an invocation boundary — a published block is org-wide,
so its internals must not reach every consumer by default. The child already writes
its own log row in the source workspace, correlated to the invoking run; the trace
existed, it just was not joined. The parent's span now carries only the child's
opaque execution id, and `hydrateChildTraces` joins the child's spans at READ time,
after authorizing the person reading against the child's workspace. Authorization
follows the viewer rather than a flag set at publish time, re-evaluates on every
read, and needs no second copy of the spans. Each hop of a nested chain is
authorized against its own workspace. Boundaries left unexpanded — no access, no
data, past a cap — say so, because a childless boundary span otherwise renders
exactly like a leaf and a partial trace reads as a complete one.

Live runs stream too, gated on `liveTraceViewerUserId`, which only surfaces with a
single known authenticated viewer set. Chat deployments stream through the same
callbacks and their consumer may be anonymous, so anything that does not opt in
keeps the boundary shut. Child spans handed to such a viewer are projected through
the CHILD's session: the invoking run's registry knows nothing about the publisher's
secrets, so projecting there would leave a source-owner credential unmasked. They
reach the live stream and stop — `createSpanFromLog` still refuses to persist them,
which is what keeps read-time hydration the single authorization point.

Environments. A fork inherits its parent's organization and `custom_block` is keyed
`(organization_id, type)`, so a uat fork resolved to the same row and ran the prod
workflow. Custom blocks become a fork-mappable resource, keyed by BLOCK TYPE — the
rule every kind follows: key by whatever the workflow references, as `file` does
with storage keys and `env-var` with names. A custom block is the only resource
referenced by the canvas block's own type rather than a sub-block value, so the
rewrite gets its own channel. Unmapped blocks keep the source type, because a type
cannot be emptied without deleting the node; they surface as unmapped and block the
promote, which is what stops uat from quietly invoking prod.

Same-named environment copies now carry their source workspace, so an Access Control
allowlist decision between three identical "Invoice Parser" rows is no longer a guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(workspace-forking): let an explicit identity custom-block mapping resolve

`remapForkBlockType` reported a mapping whose target equalled the source as
unresolved, conflating "a mapping exists" with "the type string changed". Those
are opposite states that produce an identical output `type`, and every caller
uses the flag for the former — to decide whether the reference blocks a promote.

The org-wide candidate list includes the source block, so binding an environment
to the shared block is a normal pick. Under the old flag it raised
`unmapped-custom-block` and refused the sync over a choice the user had
explicitly made. The flag is now named `resolved` and reports mapping existence;
whether the type moved is already visible from `type`.

Reported by Cursor Bugbot on #6857.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(custom-blocks): keep a streamed child's spans and markers off the parent's log

Two leaks in the live-stream path, both the same mistake: treating a channel as
viewer-scoped when it is actually persisted, so gating the stream on an
authorized viewer bought nothing.

`childTraceSpans` rode the block output to reach the stream. `filterOutputForLog`
only dropped a hidden key when the block's own config declared it
`hiddenFromDisplay` — true of the workflow block, never of a custom block, whose
outputs are publisher-curated. The source run's spans therefore persisted into
the parent's `span.output`, readable by anyone with parent-workspace access and
never re-checked by `hydrateChildTraces`. A globally hidden key is now dropped at
the top level, not only when nested, and `extractDisplayOutput` strips it again so
no other producer can reintroduce it.

The fan-out also called the invoking run's `onBlockStart`/`onBlockComplete`, which
are persist-then-emit composites: they write block names and I/O into the parent's
LoggingSession before reaching the stream. Those markers are keyed by the parent
execution and outlive the per-viewer check entirely. Custom-block children now go
through `liveStreamCallbacks`, the raw emit-only pair, and fail closed when a
surface supplies none. Same-workspace workflow children keep the composites — they
belong to the same run and their markers are legitimately the parent's.

Reported by Cursor Bugbot on #6857.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(custom-blocks): carry the emit-only stream sink into nested executions

Routing custom-block events through `liveStreamCallbacks` forwarded the viewer id
to the child but not the sink itself, so a nested hop cleared
`canStreamCustomBlockToViewer` off the inherited id and then had nothing to stream
through — `parentStreamSink` fell back to `{}` and live traces stopped at the
first sub-executor. That hit a custom block nested inside a workflow block as
readily as one inside another custom block.

The sink now travels with the viewer id, and both are withheld together when
streaming is not permitted. It is always the INHERITED chain, never
`parentStreamSink`: for a same-workspace workflow block that is the persisting
composite, so forwarding it would put a custom block nested inside one straight
back onto the parent's progress markers — the leak the previous commit closed.

Reported by Cursor Bugbot on #6857.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ave taken (#6864)

* fix(redis): reclaim a lock a timed-out acquire may have taken

`acquireLock` awaited `SET NX` and let a rejection propagate. But a rejected
SET does not mean the server declined it: the client is configured with
`commandTimeout: 5000`, and ioredis gives up locally while the command can
still reach Redis and take the lock. The caller never learns it won, so it
never releases — and since every caller treats a throw as "did not acquire",
nothing else releases it either. Every contender then skips until the TTL
expires.

Staging hit this on the Outlook polling cron: `acquireLock` threw
`Command timed out`, the route returned 500, and the next scheduled poll and
the Lambda retry both got `Polling already in progress - skipped` against a
lock whose holder had never started polling. The 180s TTL cleared it.

On failure, best-effort compare-and-delete through the existing `releaseLock`.
That deletes only while this token still owns the key, so a lock another holder
won in the meantime is untouched, and if Redis is still unreachable the TTL
stays the backstop — the behavior without this cleanup. Control flow is
unchanged for all nine call sites: the original error still propagates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(redis): make the timed-out-acquire reclaim opt-in per caller

Review caught that reclaiming unconditionally is unsafe for two caller classes,
both of which exist today:

Callers that fall open. `withLeaderLock` and the MCP OAuth refresh mutex catch a
throw from `acquireLock` and run their work uncoordinated. If the SET landed,
today the lock they hold keeps everyone else out while they run. Freeing it
under them admits a second concurrent runner — for OAuth refresh that means two
rotations of the same token and an `invalid_grant`.

Callers whose lock value is not unique. The copilot chat lock keys on
`streamId`, which is the client-supplied `userMessageId`. Two sends can carry
the same value, so a compare-and-delete from a contender that timed out can
match — and delete — the lock the active stream is holding.

Reclaiming is therefore opt-in, and the option documents both preconditions it
needs: a value unique to the holder, and a caller that does no work when
acquisition throws. Default behavior is byte-for-byte what it was before.

Opted in are the four cron/poll callers that satisfy both — webhook polling,
resume polling, workspace-events polling, and Teams subscription renewal. Each
mints its value with `generateShortId()` and returns 5xx rather than proceeding
when acquisition throws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…6868)

OpenAI answers an exhausted balance with 429, the same status as a rate limit,
but the two are not alike: a rate limit reopens and a spent account does not.
Both were classified transient, so every document burned its full retry budget
against a key that could never accept it, and because the sweep re-queues failed
documents on every sync the account turned into permanent load rather than a
one-off failure. A connector sync ran the full hour and timed out doing this.

The rejection body is what separates them — insufficient_quota, or a
credit_balance_exhausted code — so it is read when the error is built and the
retries stop immediately.

Retrying and failing over are decided separately here. An exhausted balance
rules out the key just used but says nothing about the next provider in the
chain, so the error stays eligible for failover and only the retries against the
spent key are dropped.
* feat(billing): align enterprise reporting periods

* fix(billing): harden enterprise reporting flow

* fix(outbox): preserve handler compatibility

* fix(billing): bound enterprise provisioning reads
…#6867)

* fix(provenance): stop size limits silently dropping secret provenance

A bundle-selection cap counted cells rather than rows, so a 25-column
table insert lost secret provenance for every row past 400 — the whole
batch was stamped unknown with nothing logged. The same number lived in
the sender, the runtime type guard, and the route contract.

Consolidate every provenance limit into one definition: an 8MB
serialized envelope and 10,000 distinct secrets. The pair had been
copied into seven modules under fourteen names, and several copies had
drifted into bounding inputs — rows, cells, files, chunks — rather than
the envelope.

Remove every limit that could refuse a legal payload, add write-side
cause logging and a workspace-visible audit entry when a read proceeds
on unrecorded provenance, and repair the existing unknown rows.

* fix(provenance): close a repair race and stop memory double-reporting

The repair matched sidecars by the id its page captured, so a
provenance-aware write committing between the snapshot and the delete
had its fresh exact sidecar removed and its marker cleared behind it —
a secret-bearing row left reading as legacy. The delete now re-checks
status, which under READ COMMITTED re-evaluates against the writer's
committed row so it no longer matches.

Walk the candidate set by keyset over row_id. A page whose rows were
all repaired concurrently clears nothing, and terminating on "cleared
nothing" ended the walk with the rest of the backlog untouched.

Memory reported unrecorded provenance twice, and counted records even
when the surface was enforced — auditing a fail-open read that had
actually failed closed.

* fix(provenance): take the repair's locks in the writer's order

The repair deleted the sidecar and only then updated its parent row,
while mutateTableRowsWithSecretProvenance locks user_table_rows up front
and upserts the sidecar inside the same transaction. Opposite orders, so
an overlapping write deadlocked and Postgres resolved it by aborting
either the deployment or somebody's table write.

Lock the parent first, in id order, matching lockTableRows. Holding that
lock is also what makes the status re-check decisive rather than racy:
the writer commits its sidecar and its marker under the same lock, so
once it is held the write is either wholly done or has not begun.
* fix(trigger): let workers see that Trigger.dev is available

Workers run Trigger.dev by definition, but the flag saying so is read from the
environment and had only ever been set on the app container. isTriggerAvailable()
was therefore false inside every task run, so work a task dispatched silently
took the in-process fallback instead of the queue it was written for.

Document processing is where this showed: a connector sync chunked and embedded
its documents itself, five at a time, rather than handing them to the
document-processing queue. The queue's concurrency limit, the per-document task's
machine, retry policy and duration budget all sat unused, and a sync with
thousands of documents ran until it hit its own max duration. It also explains
why that task has no runs for connector-synced knowledge bases at all.

Asserting the flag here is safe because the same check still requires
TRIGGER_SECRET_KEY, which only the Trigger.dev runtime provides: anywhere
dispatching is not actually possible the flag stays ineffective and behaviour is
unchanged.

Dispatch failure is now recoverable rather than silent. Only a total failure
raised before, so one failed batch left its documents at pending with nothing
recording why. Those are processed in-process instead, which costs the caller
the time it hoped to hand to the queue but does not drop the work. That path was
unreachable from a worker until this change made dispatching happen there.

* refactor: tighten the comments on this change and the quota classification

Both sets explained the incident that motivated the code rather than the code
itself. That kind of narrative stops being true as the surrounding system moves
and starts misleading instead, so each is cut back to the reason a reader needs.
* feat(setup): publish standalone self-hosting package

* fix(setup): refresh discovered compose installs

* improvement(setup): unify repository command

* fix(setup): harden standalone package launch

* Update README.md

* fix(setup): isolate standalone compose installs

* fix(setup): restore default stopped installs
* fix(integrations): render one brand icon state everywhere

A service glyph was drawn three different ways depending on the surface:
brand-colored via getBareIconStyle, muted through --text-icon, or left to
inherit the surrounding text color. The same Dropbox icon therefore read
blue in suggested actions and grey in the connect modal.

Replace the loose helper with a single BrandIcon component (plus
withBrandIcon for component-shaped icon slots) that owns the color, and
migrate every bare call site to it. The tiled treatment (BlockTile /
IntegrationTile) is unchanged.

* fix(integrations): update the mention chip test to the new color owner

The chip test asserted the wrapper still carried the descendant
`[&>svg]:text-*` rule that BrandIcon now owns. Assert the absence of a
competing descendant rule and check the glyph itself instead.

Also give BrandIconSlot and the test's PlainIcon dedicated props
interfaces.
…ut, and let its target inputs be configured at sync time (#6871)

* fix(workspace-forking): stop a remapped custom block losing every input

Repointing a placed custom block at another environment's block left its inputs
behind. They are keyed by the SOURCE block's Start field ids, so against the new
config they are fields that do not exist, and the serializer drops a stored value
with no matching config as a deleted input. The block synced with its name intact
and every field blank — and because both environments' blocks share a name, that
read as "the sync did nothing and corrupted the block".

The type rewrite itself was landing; a test now pins that rather than leaving it
to the eye, since a successful rewrite is visually identical.

On a type change the inputs are now replaced outright with the ones configured for
the TARGET block, and reserved wiring is preserved. There is deliberately no
attempt to migrate values across the swap: two custom blocks are independent
workflows, so a field id that happened to collide would carry a value meaning
something else. When the type does not change — no mapping, or an explicit
identity mapping — nothing is touched and values carry as they always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(workspace-forking): configure a repointed custom block's inputs at sync time

Repointing a custom block leaves it with no usable inputs — its sub-blocks are
keyed by the SOURCE block's Start field ids, which describe nothing on the new
block. Until now the user had to open the synced workflow and re-enter them by
hand, with no indication anything was missing.

A credential or table swap already makes its `dependsOn` fields reconfigurable in
the sync modal. Repointing a custom block is the same idea at its limit: not a
subset of fields is invalidated but ALL of them, so all of them are offered. They
travel the existing dependent-value channel end to end — collected into the diff,
stored per (target workflow, block, sub-block), pre-filled from the store, gating
Sync when required and empty, and applied to the written state — so nothing about
storage, pre-fill, or the Sync gate is new.

`parentKind`/`parentSourceId` are the block itself, which is already the key a
reconfig is joined to its mapping row on, so the fields render under their own
row with no extra wiring. `selectorKey`/`parentContextKey` become optional: a
custom block's inputs are typed values, not selectors, and the modal renders a
plain field (a textarea for the JSON-valued types) instead of an option list.

Deliberately no seeding from the source value: it belongs to a different block's
field of the same position, so pre-filling it would carry a value meaning
something else. A block whose type does not change is skipped entirely — its ids
still describe it, so its values carry as they always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(workspace-forking): namespace, type, and single-source a custom block's configured inputs

Four review findings, all real, three of them the same root cause: the dependent
store holds a plain string keyed only by (target workflow, block, sub-block), and
that key carried none of what applying the value correctly needs.

The key now carries the TARGET TYPE and the field's declared TYPE.

Target type, because remapping a block to A, configuring it, then remapping to B
would otherwise pre-fill and submit A's value into any field id the two happened
to share — a different workflow's field of the same name. Namespacing makes that
structurally impossible instead of a rule to remember.

Field type, because the canvas stores a boolean input as a real boolean (its
sub-block is a `switch`), so a stored `'false'` written as text is truthy to the
child workflow. The apply side reads the type off the key and restores it, and
the modal offers a switch rather than a text field. `object`/`array` stay strings:
they are authored as JSON and parsed by the executor.

Separately, the carve-out that keeps a custom block's stored value alive through
its always-true `parentChanged` was applied at the render site, so the modal
showed the stored value while the Sync gate and the submitted payload still saw
blank — required fields looked filled but kept Sync disabled, and optional ones
submitted empty and wiped the stored mapping. It now lives in
`effectiveDependentValue`, the one place all three read through.

Field-type-to-control selection moves out of the component into its own module,
where it sits beside the boolean round-trip constants it has to agree with.

Reported by Greptile and Cursor Bugbot on #6871.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(workspace-forking): keep an unset custom-block boolean unset

Boolean handling collapsed a tri-state. `''` is a flag the user never touched, and
it is not `false`.

On apply, any non-`'true'` string became `false` — so an untouched optional flag
was written as one. `assembleCustomBlockInputMapping` skips `''` but keeps
`false`, so that value reached the child's `inputMapping` and overrode whatever
default the Start field declares. Only an explicit `'true'`/`'false'` is applied
now; anything else leaves the field unset, and the child's own default stands.

In the modal the switch mapped `''` to the False segment, so a required flag
rendered as configured while the Sync gate still read it as empty — the same
display-versus-gate split the previous commit moved into `effectiveDependentValue`
to close, reintroduced one layer up. The value is passed through unmapped instead:
`''` matches neither segment, so the switch renders with nothing selected, which
is what it is.

Reported by Greptile and Cursor Bugbot on #6871.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(workspace-forking): let an optional custom-block boolean return to its default

A two-segment switch has no transition back to "nothing selected", so once a user
picked True or False there was no way to stop overriding the target workflow's
declared default — a single click pinned the flag for every later sync.

An optional boolean now carries a third `Default` segment, trailing the two real
values because choosing one is the common action and reverting is the escape
hatch. A required boolean keeps two: the Sync gate demands a value, so unset is
not a state it can end in and offering it would present an unsubmittable choice.

Reported by Greptile on #6871.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(crunchbase): add Crunchbase Data API integration

Covers the v4 Data API end to end: dedicated search and lookup operations
for organizations, people, funding rounds, and acquisitions, plus generic
collection-parameterized search and lookup reaching the remaining 39
collections, single-card paging, autocomplete, the deleted-entity feed, and
fields metadata.

Adds a crunchbase-errors extractor: the API answers failures with a bare
JSON array, which no existing extractor reads, so an auth or predicate
failure would have reported only its HTTP status.

* fix(crunchbase): honor card paging limits and cursor exclusivity

- Cap a card page at the documented 100-item maximum instead of Search's
  1000, which the shared Limit field made easy to carry over
- Always request the card's identifier so a narrowed cardFieldIds cannot
  return a full page with a null cursor and stall a paging loop
- Reject the mutually-exclusive afterId/beforeId pair on the card and
  deleted-entity endpoints, not just on search
- Report an unexpected card shape as empty rather than wrapping the
  envelope as a one-row page
…urable after the mapping is saved (#6877)

Mapping a custom block to a different block and syncing showed "no changes
required" with no fields to fill, so the inputs #6871 added were unreachable on
every sync after the one where the mapping was picked.

`parentChanged` comes from `shouldReconfigureEntry`, which asks whether the target
was edited IN THIS SESSION. Saving the mapping makes it false, and the reconfigure
listing then keeps only fields that are both required and empty — so an optional
input disappeared entirely and a filled required one never came back.

That test is right for every other kind: an unchanged credential or table mapping
leaves its stored dependent picks valid, and a Gmail label picked under the same
credential still resolves. A custom block has no such continuity. Its sub-blocks
are keyed by the SOURCE block's Start field ids, so under a different target they
describe fields that do not exist and nothing carries over — the mapping standing
IS the reason to configure, whenever it was made.

A custom block mapped to a different block is now always actionable; mapped to
itself ("keep the same block across environments") it is not, since its own field
ids still describe it. An in-session re-pick still wins over the saved target.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cbinsights): add CB Insights API v2 integration

Covers every non-streaming v2 endpoint across 25 tools: free organization
lookup, firmographics search, funding rounds and cap tables, investments,
portfolio exits, business relationships, management and board, the Mosaic /
Commercial Maturity / Exit Probability outlooks and their histories, funding
windows, revenue, strategy maps, Scouting Reports, ChatCBI, and RAG context.

CB Insights authorizes by client-credential exchange rather than a static
key, so the tools run through directExecution: the shared executor trades the
credentials for a bearer token, caches it briefly, and re-authorizes once on a
401 — the token lifetime is undocumented, so expiry is discovered rather than
predicted.

ChatCBI and RAG declare request.modelInput so an activated Sim secret in the
message is projected to its canonical label before reaching a third party's
model. directExecution still runs projectToolModelInputParams, so the two are
compatible.

The two streaming endpoints are deliberately excluded; they deliver
incremental JSON chunks and their non-streaming counterparts return the same
content in one piece.

* fix(cbinsights): reject malformed ID lists and bound the token cache

- Reject an organization ID list containing an invalid entry instead of
  dropping it. Silently filtering meant a typo ran the request against a
  narrower set — spending credits on the wrong organizations, or quietly
  widening a filtered search — and still reported success.
- Apply the same rule to the optional firmographics ID filters, where a
  dropped filter broadens the search rather than narrowing it.
- Bound the process-wide token cache so a long-lived worker serving many
  CB Insights accounts does not grow with the cumulative number of accounts
  seen. Expired entries are swept on write, then the oldest evicted.

* fix(cbinsights): stop paging and blank input bypassing the search guards

- Measure the firmographics empty-search guard against the filters alone.
  limit, nextPageToken, and sort were in the same object, so a request
  carrying only paging slipped past it and issued an unfiltered search over
  the whole database — which still spends credits.
- Reject a mistyped numeric bound instead of dropping it. A bad headcount,
  funding, or valuation filter silently widened the search, the same failure
  mode already fixed for ID lists.
- Treat an empty comma segment identically on the required and optional
  paths. A trailing or doubled comma is a separator artifact that cannot
  change which records are requested, so both paths now discard it; every
  other malformed entry is still rejected.

* fix(cbinsights): accept only plain decimal organization IDs

Number reads "0x10" as 16 and "1e2" as 100, so either notation resolved to a
real but unintended organization and the request spent credits on it. Both the
path-scoped and the bulk validators now require a plain run of digits, and use
Number.isSafeInteger so an ID past the precision limit cannot round to a
neighbouring one.

* fix(cbinsights): bound a numeric organization ID to the safe-integer range

The string path already required a safe integer; the numeric path still used
Number.isInteger, which accepts a value past the precision limit. JSON parsing
has already rounded such a value, so the request would target a different
organization than the caller supplied.
)

* feat(sidebar): add Tables and Files flyouts to the collapsed rail

Chats and Workflows already open a hover flyout on the collapsed rail;
Tables and Files were plain links. Both now list their contents, with
folders as submenus and the open resource marked.

The chip stays a real link, so clicking still opens the list page and
right-click still reaches the nav context menu. Each flyout owns its
queries and mounts only when the menu opens: a hook on the sidebar keeps
its cache subscription on every workspace route even when disabled, so
an unrelated writer would re-render the whole sidebar for a closed
flyout.

Rows are ordered by the shared sortResources, so pinned rows float and
the flyout reads in the same order as the page it links into.

Also removes two dead components (CollapsedFileFolderItems, FileList)
that were exported but never rendered, and extracts SidebarNavChip so
the rail chip has one definition.

* fix(emcn): stop ordinary menus scrolling at the shared height cap

Every DropdownMenuContent was capped at a flat 240px. A menu is 28px per
row, 13px per separator, plus 12px padding, so a 7-row action menu with
3 separators measures 247px and scrolled for 7px while the 7-row menu
beside it with 1 separator did not.

Raises the cap to 420px, which clears every hand-authored action menu,
and clamps it with min() against the space Radix measures so a menu near
a viewport edge stays on screen — which the flat value never did. The
cap still exists so a long data-driven list scrolls instead of running
the height of the screen.

* fix(sidebar): hold the rail flyout until its lists resolve for this workspace

Both the resource and folder queries keep the previous workspace's rows as
placeholder data across a switch. Gating only on isPending let the flyout
build a tree from one workspace's resources against another's folders, where
no folder id resolves — which the builder reads as "archived out from under
it" and files the whole list at the root.

Gate on isPlaceholderData too, matching foldersResolved in
use-folder-ancestors. An error settles a query without resolving it and is
deliberately not held: the flyout then renders flat, which still reaches
every row.

* improvement(sidebar): mark pinned rows in the rail flyout

The flyout sorts pinned rows to the top via the shared sortResources, but
rendered no indicator, so that ordering read as arbitrary — the exact
pairing Resource's own label cell documents. Carry `pinned` on each row
and render the same non-interactive glyph, on folders as well as
resources.

Adds folder-structure coverage alongside it: per-level ordering, the full
depth of a nested chain, and an empty folder staying in the tree.
…lidation (#6880)

* feat(granola): complete API coverage, note triggers, and validation fixes

Granola's public API exposes nine endpoints; Sim implemented three. Adds the
remaining six and wires the new programmatic webhook-endpoint lifecycle into a
managed trigger.

Tools (6 new, 9 total):
- get_transcript, list_audit_events
- create/list/update/delete_webhook_endpoint

Triggers: note.generated, note.edited, note.access_granted, plus an all-events
trigger. The provider handler registers the Granola endpoint on deploy and
deletes it on undeploy, scoped to the trigger's own event names, and verifies
every delivery with the Standard Webhooks HMAC-SHA256 signature Granola returns
on creation. event_id is the idempotency key, which Granola reuses across
retries.

Validation fixes to the shipped tools:
- get_note dropped speaker.attribution ("me"/"them"); now surfaced
- a 413 on get_note now explains that the transcript is too large inline and
  points at get_transcript, instead of surfacing a bare status code
- note IDs are URL-encoded rather than interpolated raw
- base URL, auth headers, and status-aware error handling are shared runtime
  helpers; params/outputs stay literal per file so the docs generator still
  reads them

Tests cover signature verification (including replay and body-tamper
rejection), event matching, subscription create/delete, and the block/tool
contract — plus a guard that ids shared between the tool and trigger surfaces
seed the same default, since block state is keyed by id and last-wins.

The knowledge-base connector was validated against the spec and needed no
changes.

* fix(granola): correct array output schemas, listing-truncation signal, and docs

Findings from validation passes over the tools, trigger, and connector.

Tools — array outputs were declared as `type: 'json'` with `properties`, which
describes an object, not an array. Agents and the output picker therefore saw
`notes.title` instead of `notes[i].title`. All 15 array outputs (including the
pre-existing three tools) now use `type: 'array'` with `items`, matching the
2000+ other tool files. The audit event `data` field stays `json`; it is
genuinely free-form per the spec.

Connector — `hasMore` was ANDed with the cursor, so a `hasMore: true` response
with no cursor was reported as a complete listing. The sync engine treats
exactly that shape as truncated and sets `listingTruncated` to block deletion
reconciliation; masking it meant a partial first page could be taken for the
whole corpus and reconciliation would hard-delete every note past it. Granola
would have to violate its own contract to emit that shape, but the engine
already handles it and the connector was hiding the signal. Also aligns
mimeType with the `.txt`/text-plain bytes the engine actually writes (it was
the only connector of 101 claiming text/markdown).

Trigger — the setup instructions named a Granola settings path that does not
exist; the help center says Settings > Connectors > API keys in the desktop app.

Both list parsers now split commas inside array entries, so an array-wrapped
free-text value cannot be sent as one malformed identifier.

Block — `id`, `events`, and `hasMore` are produced by several operations but
their descriptions named only one, unlike `folders` which already documented
both meanings.

Adds connector tests pinning all four listingCapped quadrants and the
truncation signal, and tool tests for the list parser and the PATCH body's
per-field "omit means unchanged" semantics.

* fix(granola): clean up webhook endpoints created by a failed registration

Raised independently by both reviewers. The registration service only rolls
external state back when createSubscription *returns* — its rollback is guarded
on `preparedProviderConfig`, so a handler that throws is assumed to have left
nothing behind. Granola's handler broke that contract: when Granola accepted the
POST but the success body was missing `id` or `signing_secret` (including a body
that failed to parse and became `{}`), it threw with the endpoint already live.

Nothing then recorded an external id, so undeploy could not remove it, and
Granola kept delivering to a callback whose signature could never be verified —
duplicating on every deploy retry.

The handler now removes what it created before rethrowing, matching the pattern
grain's multi-hook create already uses. It deletes by id when Granola returned
one, and otherwise recovers the endpoint by matching the callback URL, which
also covers a connection that fails after the request reached Granola.
Endpoints whose URL was redacted to its origin are never matched — that
comparison could delete another workflow's endpoint on the same host. Cleanup is
best effort and never masks the original failure. A non-2xx is left alone, since
no endpoint was created.

Also folds the delete call shared with deleteSubscription into one helper.

* fix(granola): never recover an orphaned endpoint by callback URL

The previous commit's URL-based recovery was unsafe. A redeploy reuses the live
registration's `path`, so the candidate and the currently serving endpoint share
a callback URL — listing by that URL and deleting every match would remove the
live deployment's endpoint and silently stop a working trigger, which is worse
than the leak it was trying to prevent.

Cleanup is now keyed solely on the id Granola returned. When the success body
carries no id there is no way to tell the candidate's endpoint from the live
one, so it is left in place: a leaked endpoint produces unverifiable deliveries
that Granola disables on its own, whereas deleting the wrong one takes down live
traffic with no signal.

The 2xx-missing-signing-secret case this originally fixed still cleans up, since
that response does carry an id.

Adds a test asserting no lookup or delete is attempted when the response has no
id, so URL matching cannot be reintroduced unnoticed.
* feat(integrations): add Bitbucket Cloud

* fix(bitbucket): enforce selector workspace slugs

* fix(bitbucket): overfetch small pipeline log tails

* fix(bitbucket): harden provider edge cases

* fix(bitbucket): accept provider diff redirect specs

* fix(bitbucket): stop advanced-field leakage and harden log, status, and selector paths

Splits the `closeSourceBranch` advanced subBlock into per-operation ids. Advanced
fields serialize without evaluating their condition, so a value set on Create Pull
Request reached Merge Pull Request and closed the source branch unprompted.

Also:
- read step logs through the byte-capped server transport and map an empty-log 416
  to an empty result, keeping a genuine 416 an error
- trim a step log's partial leading line after the character cap rather than before,
  and never return an empty log when the retained window held content
- surface Bitbucket's `error.detail` alongside `error.message`
- treat commit-status `key`/`state` as nullable so one malformed row cannot drop a page
- match repository `full_name` case-insensitively and reject dot segments in a
  workspace slug before the outbound request
- type `reviewerAccountIds` as the comma-separated string it is
- trim optional Bitbucket query strings; correct the token lifetime to two hours

---------

Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Waleed Latif <walif6@gmail.com>
* fix(workflow): prevent canvas slowdown cascades

* fix(workflow): make connection picker scrolling seamless

* fix(workflow): correct two regressions in the canvas perf pass

Gating `toolBlocks` on the picker's open state also emptied it for the
always-visible selected-tool chips, which silently fell through to their
`getBlock` fallback — the branch documented as the exception for types
hidden from the picker. Only `toolGroups`, where the expensive group build
lives, is gated now.

Re-invoking the find shortcut while the panel was already open stopped
re-selecting the query: `open()` is a no-op when the panel is mounted, so
the mount-time focus effect never re-ran. The panel publishes its focus
callback so the shortcut can drive it either way.

A saturated `slice` also allocated a fresh array once the limit covered a
whole group, re-rendering the memoized "All blocks" group on every tools
page-in — the frame cost the change set out to remove.

Alongside those: fold the two reconcilers into one generic and decide reuse
by identity rather than a three-write `changed` flag; record why the node
comparison is deliberately asymmetric (React Flow augments node objects in
place, so a symmetric `isEqual` would never reuse anything); give the
browse pagination its own constant instead of borrowing the search-result
cap; drop a redundant clamp and the deps it needed; inline the
single-consumer `sliceGroupsToLimit`; and split the bundled ref so the
hottest component stops allocating an object per render.

---------

Co-authored-by: Waleed Latif <walif6@gmail.com>
…r a remote option list, and close the fork-sync reconfiguration gap (#6878)

* fix(workspace-forking): stop double-labelling a custom block's inputs, and derive their controls from the canvas

Two problems with how a repointed custom block's inputs render in the sync modal.

The field title printed twice. The row wrapper already draws the label and its
required marker for every dependent field — `DependentFieldSelector` takes a
`title` only to phrase its placeholder and renders a bare combobox. The
custom-block branch used `ChipModalField`, which owns a label of its own, so every
input showed its name twice. It now renders bare controls like its sibling does.

The control was chosen by re-reading the raw field type instead of asking the
function that already answers this. `subBlockTypeForField` decides what a Start
field becomes on the canvas; the modal had a parallel switch that had already
drifted, rendering a `file[]` input — an upload on the canvas — as a plain text
box, which would write a bare string into a field expecting file references.

`subBlockTypeForField` is now exported and the modal derives from it, so the two
cannot disagree about what a field IS; the modal only decides how that kind draws.
A file input is explicitly `unsupported` rather than falling through: it renders
disabled, saying it is set in the workflow, instead of inviting a value that
cannot work. A test walks every type a Start field can declare and asserts the
modal's choice follows the canvas's, so a type added later surfaces here rather
than silently becoming a text box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(workspace-forking): resolve a custom block's inputs against the target environment, and stop a re-sync wiping its uploads

A repointed custom block's inputs are configured at sync time, but the modal
drew them as bare text fields against no environment at all:

- `{{SECRET}}` had no completion, and no way to know which secrets exist in the
  workspace the value is written INTO.
- `<block.output>` had no completion. The canvas dropdown reads the workflow
  open in the editor; on the fork settings page there is none, and the workflow
  that matters is the target's.
- A `file[]` input has no control here (it is an upload on the canvas), so it
  had no stored override — and the block was rebuilt from overrides alone, so
  every sync silently dropped the target's uploaded files.

`WorkflowReferenceScope` lets a surface supply the workflow a reference resolves
against. Absent a provider, the hooks read the live editor stores exactly as
before, so the canvas is unchanged. The scope splits graph from values on
purpose: reachability cannot change with the text being typed, and the
validation hook runs in every reference-aware sub-block editor at once, so
subscribing it to live sub-block values would re-render all of them on every
keystroke. A test pins that split.

`replaceCustomBlockInputs` now seeds from the target block when it is ALREADY
the mapped type, layering the configured values on top. That keeps an input the
modal cannot offer a control for, and leaves a field the user simply did not
touch alone; a field they explicitly emptied stores `''`, which is an override
and still wins. Under a DIFFERENT current type nothing is carried over — those
values are keyed by another block's field ids, which is the orphaning this
function exists to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(workspace-forking): stop a required file input deadlocking Sync

Both PR bots flagged this and they were right. A repointed custom block's
`file[]` input renders as a disabled control — it is an upload on the canvas,
and there is nothing to type here — but the Sync gate still demanded a
non-empty value for every REQUIRED dependent. So a custom block with a required
file input turned Sync off permanently, while the field's own hint told the
user to go set it in a workflow they could only reach BY syncing.

`isForkSyncConfigurableField` is the one predicate for "can the modal put a
value in this field", used by the gate and by the per-kind status badge so the
two cannot disagree. Skipping the gate is only safe because the sync no longer
clears the field: the target keeps what it has, and a genuinely missing value is
still caught by the block's own required-field validation at run/deploy time —
the same fallback every other unconfigured required field already relies on.

Also gives the disabled control an `aria-label` (the row's visible label is a
sibling, not associated), closing the second review note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(sub-blocks): make a registered selector the single source for a remote option list

`dropdown` and `combobox` could only load a remote list through a per-block
`fetchOptions(blockId)`, which resolves its credential by reading the live
workflow store. That works on the canvas and nowhere else — which is why the
fork sync modal cannot offer those fields, and why every one of those fetchers
turned out to be a hand-rolled duplicate of a selector that already exists
(`triggers/gmail/poller.ts` calls the very contract `gmail.labels` wraps).

Both controls now accept `selectorKey`, resolved through the registry inside
`useFetchedOptions`. Deliberately NOT a second code path: the registry is
presented through the same two function shapes the props already describe, so
the existing lifecycle — request-id guards, dependency-scope reset, label
hydration — is reused verbatim, and paginated selectors drain through the same
`loadAllSelectorOptions` that search/replace and value resolution already use.

`isDynamic` replaces the `fetchOptions &&` test the controls used to decide
whether the fetched list or the static `options` array is authoritative; that
question outlives the prop it was asking about.

No block or trigger changes yet, so nothing moves off `fetchOptions` in this
commit: subblock `type`, `multiSelect`, and the stored value shape are all
untouched and no existing workflow is affected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(triggers): move every credential-scoped option list onto a registered selector

Each of these `fetchOptions` resolved its credential with
`readSubBlockValue(blockId, 'triggerCredentials')` — a live-workflow-store read
— and then called the very selector contract a registered selector already
wraps. They were duplicates that only worked on the canvas.

Migrated: webflow sites/collections (x4 triggers), clickup workspaces, gmail
labels, outlook folders, and all six hubspot pickers. 425 lines of duplicated
fetch logic deleted.

The missing piece each one needed was `canonicalParamId: 'oauthCredential'` on
its credential subblock: `buildSelectorContextFromBlock` keys the context on a
subblock's CANONICAL id, so without it `context.oauthCredential` was never
populated and the block had no way to reach its credential except the store —
which is what forced the hand-rolled fetcher in the first place.

Five new hubspot selectors. `hubspot.pipelineStages` reads the pipelines
contract and narrows, because HubSpot returns stages inside the pipeline
payload rather than behind an endpoint of their own; sharing the one response
is also what keeps a stage list from ever describing a pipeline its sibling
picker is not showing. `objectType`/`customObjectTypeId`/`pipelineId` join
SelectorContext, and `resolveObjectType` keeps HubSpot's own `contact` default
so an untouched dropdown still lists properties for what it visibly shows.

Subblock `type`, `multiSelect`, and stored value shapes are unchanged, so
existing workflows are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(triggers): move the table trigger's column picker onto table.columns

`fetchTableColumns` resolved the workspace from the active-workflow store and
the table id by reading two subblocks by name, then refetched the table list to
find one table's schema. The registered `table.columns` selector takes both from
the context — `tableSelector`/`manualTableId` already carry
`canonicalParamId: 'tableId'`, so the canonical pair resolves on its own — and
reads the table detail query directly.

Deletes the helper and the four imports it was the only user of.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(managed-agent): move its four pickers onto registered selectors

All four read one route distinguished only by `resource`, with the credential
pulled from the store by name. They are now `managedAgent.agents` / `.vaults` /
`.memoryStores` / `.environments`, and `lib/managed-agents/subblock-options.ts`
is deleted entirely.

The environment filter (cloud vs self_hosted expose different fields, so mixing
them offers choices the rest of the form cannot honour) moves into the selector
with `environmentType` on the context.

Also decouples two things `canonicalParamId` was conflating. It is both a
block's serialized PARAM NAME and the key `buildSelectorContextFromBlock` reads,
so making this block's pickers resolvable appeared to require renaming its
shipped `credential` param to `oauthCredential` — a rename that would change the
serialized shape of every existing managed_agent block, and one that
`blocks.test.ts` correctly refused. A picker should not be able to force a param
rename, so the context now reads a credential off the subblock TYPE when no
canonical id supplied one. It only fills a gap: a block that declares
`canonicalParamId: 'oauthCredential'` has already resolved it, including the
basic/advanced active-member logic the type check cannot express.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(sub-blocks): delete fetchOptions — a sub-block's options are a selector or derived, never both

Completes the migration. `fetchOptions`/`fetchOptionById` are off `SubBlockConfig`,
off both controls, and out of `useFetchedOptions`, leaving exactly two ways a
sub-block gets its options:

  selectorKey  — a registered selector. The ONLY way to load a remote list.
                 Parameterized by an explicit SelectorContext, so it works on the
                 canvas, in the fork sync modal, and anywhere else.
  options      — a static array, or a pure function of the block's own values.
                 No I/O.

Reading the remaining callsites showed most of the "derived" ones were nothing of
the kind — they were workspace-scoped remote fetches wearing a local-looking
signature. Those became seven `workspace.*` selectors (credential providers,
credential groups + their per-group providers, secret names, raw secret names,
sandboxes, trigger types) plus `providers.openrouterEmbeddingModels`. Only the
agent block's three capability dropdowns were genuinely derived; `options` now
takes the block's values so they can say so directly. The parameter is optional,
so every existing zero-argument options function is untouched.

`imap.mailboxes` is the one selector whose account is typed rather than stored.
Its password is deliberately absent from the query key: a query key identifies a
resource, a credential authorizes access to it. `oauthCredential` is safe there
because it is only an id — a typed password is a secret, and keys are cached and
surfaced by devtools. Host, port, TLS and username already identify the mailbox
list uniquely; the password rides the body exactly as before.

`selectorExcludeSelf` replaces the one thing a shared `sim.workflows` selector
could not express. It is a declared flag rather than a blanket rule because the
answer differs per field: the Sim trigger never receives events about its own
workflow, while the Logs block legitimately reads the logs of the workflow it
runs in.

Deletes `lib/workflows/subblocks/options.ts` and `triggers/editor-state.ts`
entirely — every caller was a `fetchOptions` resolver. The live-registry test for
the trigger vocabulary moves to the selector that now owns it, keeping its
lazy-import cycle guarantee under test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(workspace-forking): make every fork-clearable sub-block reconfigurable at sync time, and lint that it stays so

`clearDependentsOnRemap` wipes every transitive dependent of a remapped parent,
and a credential mapped between environments changes value on EVERY sync — so a
dependent the sync modal could not offer was re-emptied on every push, with
nowhere to set it that stuck. Setting it in the target did not survive. 36 fields
were in that state.

The selector migration closed most of it; this closes the rest. The collector now
also emits plain text dependents (`short-input` / `long-input`), which need no
selector — just somewhere to type — and the modal's no-selector branch renders
them through the same control it already drew for custom-block inputs. It
deliberately does NOT emit the manual half of a selector-backed canonical pair:
that pair already represents the field once, and its manual member is verbatim by
policy, so offering both would show one concept twice and invite writing into the
inactive half.

`forkDependentControl` replaces the direct `customBlockInputControl` call in the
view, because `fieldType` now means two different things: a custom-block input
declares a Start FIELD type (`string`, `file[]`), while every other no-selector
dependent is a canvas SUB-BLOCK whose own type says it. They agreed by accident
before; now they are classified separately.

`check:fork-dependent-coverage` fails when a sub-block under a
credential/knowledge-base/table anchor is none of: selector-backed, a canonical
pair member, a preserved name-based type, or text. 656 dependents, zero
uncovered, no baseline — verified to fail by seeding a regression. Picked up
automatically by `check:audits` (all 30 green).

Documented in `/add-block`, `/add-trigger`, and `.claude/rules/sim-integrations.md`,
including the two rules the checks enforce: a secret never enters a selector's
query key, and a fork-clearable dependent must be reconfigurable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sub-blocks): stop selector-backed fields rendering undefined options, and pass derived values to ComboBox

Both found by Bugbot on the migration commit; both real, both mine.

A selector-backed field carries no static `options` — that is the point — but
`Dropdown` and `ComboBox` still read it on first paint, before any fetch resolves,
and `allOptions.map(...)` is unconditional. Every field moved to `selectorKey`
(Function sandboxes, Managed Agent pickers, OpenRouter embeddings, Logs
workflows, the migrated triggers) would throw on mount. The type said the prop was
required, so nothing caught it: the callsites pass `config.options`, which is
optional on `SubBlockConfig` and now genuinely absent.

Fixed on the controls rather than by restoring `options: []` to every migrated
sub-block: the absence is correct, so the component owns the default. `options`
is optional on both prop types and falls back to a shared empty array, which also
keeps a stable identity for the memo.

`ComboBox` never got the `options({ values })` wiring `Dropdown` received, so
agent's reasoning-effort, verbosity and thinking-level lists — all comboboxes —
silently stayed on their generic fallback instead of narrowing to the selected
model. Wired the same way, reading the block's own values from the store.

`selector-backed-subblocks.test.ts` pins the invariants against the real registry:
a named selector exists and can list, a selector-backed field never also declares
static options, and a field whose selector is gated on context declares the
`dependsOn` that rebuilds it. That last one immediately caught a third bug —
`clickup.triggerWorkspaceId` had no `dependsOn`, so its list would have loaded
once, empty, and never refetched once a credential was picked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(selectors): restore the credential-group provider label resolver, and probe getQueryKey for missing dependsOn

Two findings from a final adversarial pass over the migration, both the same
class as the three the review bots caught: something a `fetchOptions` sub-block
declared that its replacement selector quietly does not.

`credential-group.providerFilter` had a `fetchOptionById`;
`workspace.credentialGroupProviders` had no `fetchById`, so the canvas card
summarising several stored provider ids lost every label. The field is
multi-select, which is exactly when a label has to resolve without the full list.

The `dependsOn` assertion in `selector-backed-subblocks.test.ts` only probed
`enabled` against three hand-listed context fields, which is why it caught
`clickup.triggerWorkspaceId` and would have missed the rest. It now probes
`getQueryKey` as well — a selector's key names every context field its RESULT
depends on — and derives the sub-block-sourced set from
`SELECTOR_CONTEXT_FIELDS` rather than a literal. Verified by deleting a real
`dependsOn`: it fails naming the field and the fields it depends on.

Also checked and NOT changed: `display.ts` and the copilot dropdown validator
both guard `options` before use, so stripping `options: []` does not reach them.
The validator's behaviour does shift from "reject every value" (an empty
`validIds` array matched nothing) to "skip validation", which is a relaxation
rather than a regression. `function.sandboxId` kept its `dependsOn: ['language']`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: re-record the page-graph baseline after staging's growth consumed its allowance

CI's "Repo audits" step failed on `check:tool-registry-boundary`. Measured before
touching anything, because the reported growth (+32 and +42 modules on two
routes) looked like this branch had dragged the selector registry somewhere new.

It had not. Recording a baseline on clean `origin/staging` and diffing against
this branch attributes the growth precisely:

  this branch:  +1 to +4 modules per route, +35 total across 25 routes
  staging:      the rest

Staging's six merged commits landed both failing routes at exactly their
tolerance — knowledge/[id] at +31 of an allowed +31, layout at +41 of +41 — so
`check:tool-registry-boundary` passed there with nothing left over. This branch's
+1 tipped both past the line. The next PR to touch anything would have tripped it
just the same, whatever it contained.

The +1..+4 is the selector consolidation's real cost: `selectorRegistry` is one
static object, so a page reaching any selector reaches every provider, and this
branch adds four (hubspot, managed-agent, imap, workspace). That is the same cost
the 27 existing providers already impose, and it is what buys one option-list
mechanism that works off the canvas.

Also tried deferring the workspace provider's data-layer imports to fetch time.
Reverted: this checker follows dynamic imports, so the numbers did not move,
leaving only a Promise.all-of-imports shape that reads worse than the 27 sibling
providers it sits next to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(workspace-forking): actually apply text dependents, and resolve labels against the full selector context

Both from Bugbot; both real, both mine.

**Text dependents never persisted.** `applyDependentOverrides` allowlisted
`dependsOn && selectorKey`, so the plain text fields the collector started
emitting were offered in the modal, stored, and gated on by the Sync button —
then dropped on apply. The field stayed wiped on every push and the typed value
went nowhere, which is the exact treadmill the feature existed to end.

The cause was the rule being written twice. `reconfigurableDependentIds` is now
the single definition of "a dependent the modal can offer AND the sync can write
back", used by the collector and by the apply side. A test asserts the two agree
by round-tripping through `applyDependentOverrides`, and fails against the old
allowlist.

**Provider labels stayed raw ids.** `useDynamicSubBlockOptionDisplayName` called
`fetchById` with a `workspaceId`-only context, which silently fails any selector
scoped by a sibling — `workspace.credentialGroupProviders` needs the group before
it can name a provider, so the `fetchById` restored last round returned null
every time. It now builds the block's real context with
`buildSelectorContextFromBlock`, the same one the canvas uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(queries): scope the sub-block label cache by the selector's own context

Follow-on to 1f423ab, and a real gap in it. That commit taught `fetchById` to
read sibling context but left the React Query key at
`(workspaceId, blockId, subBlockId, optionId)`. A label resolved before its
sibling was set — `workspace.credentialGroupProviders` with no group picked,
which returns `null` — stayed cached under the same key and was reused once the
group WAS picked, so the card kept showing the raw id. Changing between two
groups collided the same way.

This is the repo's own React Query rule ("every identifier the queryFn forwards
into the fetch must appear in the queryKey"); `check:react-query` did not catch
it because the context is built in the hook rather than passed as a named arg.

The key now carries the selector's OWN `getQueryKey` for that context, rather
than a second hand-maintained list of context fields. The cache is scoped by
exactly what the selector reads, and stays correct if a selector's dependencies
change later. The context also became reactive (subscribed rather than read via
`getState()`), which is what lets the key move when the sibling does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* perf(react): reduce SVG path precision

* fix(react): preserve Sim wordmark precision

* fix(icons): preserve Quartr scale

* test(icons): ratchet SVG path precision

* fix(icons): make precision exceptions local

* perf(icons): enforce three-decimal paths
…6883)

* fix(bitbucket): bind cursors and task locations case-insensitively

Bitbucket resolves workspace and repository slugs case-insensitively but echoes
the canonical lowercase form in `next` links, diff/diffstat redirect targets, and
async merge task Locations. Binding those back with exact string equality meant a
mixed-case slug succeeded on the first request and then failed on every follow-up
— worst on a 202 merge, where the merge has already started when polling breaks.

Repository file paths keep verbatim comparison; git treats those as case-sensitive.

* fix(bitbucket): fold only the slug segments Bitbucket canonicalizes

Segment-wise comparison replaces whole-path case folding, so a cursor that recases
a fixed endpoint literal (repositories, commits, pullrequests) fails locally again
instead of being deferred to Bitbucket. Only the workspace and repository segments
of a /2.0/repositories path fold; file paths and literals stay verbatim.
… mark (#6884)

CB Insights moves from a dark navy tile to white, matching Jira, Confluence, and
Bitbucket. Its icon carries its own fills, so it stays legible on the lighter tile.

The Crunchbase icon drops the white rounded-square plate and its border, leaving
just the `cb` mark on `currentColor` so the block's bgColor supplies the tile. The
viewBox is retargeted to the glyph's true curve extrema with padding that keeps it
at the same optical weight as the surrounding brand marks.
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner August 20, 2026 05:45
@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (705 files, 100 file limit).

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 20, 2026 6:39am

Request Review

@cursor

cursor Bot commented Aug 20, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Changes are documentation, README onboarding, and a new publish workflow for an existing package; no runtime app logic in this diff.

Overview
This diff updates authoring docs and CI to match the standalone @sim/setup / packages/deployment-config layout, and documents the selector-based option list rules from the sub-blocks refactor.

Deployment metadata paths — Agent skills (add-block, add-integration, add-trigger, validate-integration), sim-integrations.md, and checklists now point OAuth/deployment catalog work at packages/deployment-config (integrations.json, env-capabilities.ts, generated service-account maps) and CLI field mapping at packages/sim-setup/src/capability-config.ts. They require bun run deployment-config:generate and bun run deployment-config:check alongside existing docs/catalog gates, and use npx @sim/setup add integration instead of bun run setup integration.

Option lists (new sections)add-block, add-trigger, and integration rules state that remote lists must use selectorKey (with canonicalParamId: 'oauthCredential' on credential fields); static or pure options only otherwise—no fetchOptions or store reads from block definitions. check:fork-dependent-coverage is documented for fork-sync reconfiguration.

Publish pipeline — New workflow .github/workflows/publish-sim-setup.yml publishes @sim/setup on pushes to main/staging/dev when setup-related paths change: deployment-config:check, package tests, type-check, build, channel versions (dev/preview/stable), pack smoke tests (no repo source in tarball, prod Compose without local build), then npm publish.

README — Quickstart self-host is npx @sim/setup (Node 20+ + Docker, no clone); repo contributors use bun run sim-setup for local dev/K8s modes.

Reviewed by Cursor Bugbot for commit f6a9f0d. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f6a9f0d. Configure here.

Comment thread .github/workflows/publish-sim-setup.yml
* fix(setup): publish unscoped setup package

* fix(setup): strip renamed status command
@waleedlatif1
waleedlatif1 merged commit ceda457 into main Aug 20, 2026
41 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants