You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: .agents/skills/add-block/SKILL.md
+31Lines changed: 31 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1052,3 +1052,34 @@ After creating the block, you MUST validate it against every tool it references:
1052
1052
4.**Verify conditions** — each subBlock should only show for the operations that actually use it
1053
1053
5.**Verify `{Service}BlockMeta` is exported** with at least 7 templates, each having `icon`, `title`, `prompt`, `modules`, `category`, and `tags`
1054
1054
6.**If any tool outputs are still unknown**, explicitly tell the user instead of guessing block outputs
1055
+
1056
+
## Option Lists: `selectorKey` or `options`, never a per-block fetcher
1057
+
1058
+
A sub-block gets its choices from exactly one of two places. There is no third.
1059
+
1060
+
**`selectorKey` — every remote list.** Register the list in `hooks/selectors/providers/<service>/selectors.ts`, add its key to `SelectorKey`, and point the sub-block at it. A selector is parameterized by an explicit `SelectorContext`, so the same definition serves the canvas, the workspace-fork sync modal, and anything added later.
`canonicalParamId: 'oauthCredential'` on the credential sub-block is the line people forget. `buildSelectorContextFromBlock` keys the context on a sub-block's CANONICAL id, so without it `context.oauthCredential` is never set and the picker looks unfixable without reading the store. (A credential field is also recognised by its `oauth-input` TYPE as a fallback, so a block whose shipped param is already named something else does not have to rename it.)
1070
+
1071
+
**`options` — everything else.** A static array, or a pure function of the block's own values for a list that narrows to a sibling's selection. No I/O.
**Never fetch inside `options`, and never reach into the stores from a block definition.** A fetcher that resolves its credential with `readSubBlockValue(blockId, ...)` only works on the canvas — every surface that is not the editor gets an empty list. `fetchOptions`/`fetchOptionById` were removed for exactly this reason.
1081
+
1082
+
Two rules the checks enforce:
1083
+
1084
+
-**A secret never enters a selector's `getQueryKey`.** A query key identifies a resource; a credential authorizes access to it. A credential *id* is fine; a typed password is not (see `imap.mailboxes`).
1085
+
-**A sub-block that `dependsOn` a credential / knowledge-base / table selector must be reconfigurable at fork-sync time** — a `selectorKey`, a canonical pair whose basic member is a selector, or a `short-input`/`long-input`. `bun run check:fork-dependent-coverage` fails otherwise, because a fork sync clears those fields on every push and an unofferable one can never be set anywhere that sticks.
## Option Lists: `selectorKey` or `options`, never a per-block fetcher
476
+
477
+
A sub-block gets its choices from exactly one of two places. There is no third.
478
+
479
+
**`selectorKey` — every remote list.** Register the list in `hooks/selectors/providers/<service>/selectors.ts`, add its key to `SelectorKey`, and point the sub-block at it. A selector is parameterized by an explicit `SelectorContext`, so the same definition serves the canvas, the workspace-fork sync modal, and anything added later.
`canonicalParamId: 'oauthCredential'` on the credential sub-block is the line people forget. `buildSelectorContextFromBlock` keys the context on a sub-block's CANONICAL id, so without it `context.oauthCredential` is never set and the picker looks unfixable without reading the store. (A credential field is also recognised by its `oauth-input` TYPE as a fallback, so a block whose shipped param is already named something else does not have to rename it.)
489
+
490
+
**`options` — everything else.** A static array, or a pure function of the block's own values for a list that narrows to a sibling's selection. No I/O.
491
+
492
+
```ts
493
+
options: (params) => {
494
+
const model = params?.values.model
495
+
return typeof model === 'string' ? effortsFor(model) : DEFAULT_EFFORTS
496
+
}
497
+
```
498
+
499
+
**Never fetch inside `options`, and never reach into the stores from a block definition.** A fetcher that resolves its credential with `readSubBlockValue(blockId, ...)` only works on the canvas — every surface that is not the editor gets an empty list. `fetchOptions`/`fetchOptionById` were removed for exactly this reason.
500
+
501
+
Two rules the checks enforce:
502
+
503
+
- **A secret never enters a selector's `getQueryKey`.** A query key identifies a resource; a credential authorizes access to it. A credential *id* is fine; a typed password is not (see `imap.mailboxes`).
504
+
- **A sub-block that `dependsOn` a credential / knowledge-base / table selector must be reconfigurable at fork-sync time** — a `selectorKey`, a canonical pair whose basic member is a selector, or a `short-input`/`long-input`. `bun run check:fork-dependent-coverage` fails otherwise, because a fork sync clears those fields on every push and an unofferable one can never be set anywhere that sticks.
Copy file name to clipboardExpand all lines: .claude/rules/sim-integrations.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,4 +17,5 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
17
17
- Type coercions (`Number()`, etc.) belong in `tools.config.params` (runs at execution, after variable resolution) — never in `tools.config.tool` (runs at serialization; coercing there destroys dynamic `<Block.output>` references).
18
18
-`canonicalParamId` must NOT match any subblock's `id`, must be unique **block-wide** (groups are keyed by canonical id across every subblock and hold exactly one `basicId`, so two operations that each need a pair need two different canonical ids), and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs — the serializer deletes the subblock IDs and republishes the active member's value under the canonical ID.
19
19
- A canonical pair carries ONE concept. For files that is upload (basic) + file reference (advanced), as in Gmail attachments (`blocks/blocks/gmail.ts`). Never overload the advanced side with alternate identifiers (URL, provider asset ID) — give those their own subblocks, mark mutually exclusive sources `required: false`, and enforce "exactly one" at execution.
20
+
- A sub-block's option list is EITHER `selectorKey` (a registered selector — the only way to load a remote list, and the only one that works off the canvas) OR `options` (a static array, or a pure function of the block's own values). Never fetch from a block definition, and never read the workflow stores there. A credential sub-block needs `canonicalParamId: 'oauthCredential'` for its dependants' selectors to resolve. A secret must never appear in a selector's `getQueryKey`. `bun run check:fork-dependent-coverage` fails a `dependsOn` under a credential/KB/table anchor that the fork sync modal cannot offer.
20
21
- Blocks must also set the catalog/UI metadata fields `integrationType`, `tags`, `authMode`, `docsLink`, and export a `{Service}BlockMeta` — see the `/add-block` skill's BlockMeta section for details.
0 commit comments