Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .agents/skills/wrdn-effect-atom-reactivity-keys/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: wrdn-effect-atom-reactivity-keys
description: Add reactivityKeys to effect-atom write mutation calls. Use when lint flags a useAtomSet mutation call that mutates data without invalidation keys.
allowed-tools: Read Grep Glob Bash
---

Effect-atom write mutations must say which reads they invalidate.

## Fix Shape

- Find the `useAtomSet(...)` write mutation call.
- Add `reactivityKeys` to the mutation payload at the call site.
- Use the narrowest keys that cover the rows/lists affected by the write.
- Keep read-only probe/preview OAuth flows out of this pattern.
- If the mutation should update UI immediately, check whether `wrdn-effect-atom-optimistic` also applies.

## Good

```ts
await updateSource({
params: { scopeId, sourceId },
payload,
reactivityKeys: [["sources", scopeId]],
});
```
115 changes: 115 additions & 0 deletions .agents/skills/wrdn-effect-promise-exit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
name: wrdn-effect-promise-exit
description: Replace React/effect-atom mutation handlers that use promise-mode plus try/catch with promiseExit and explicit Exit handling. Use when lint or review flags try/catch around useAtomSet mutation calls, especially UI handlers that set error/busy state after a failed mutation.
allowed-tools: Read Grep Glob Bash
---

You fix one pattern: a React handler awaits an effect-atom mutation in `mode: "promise"` and catches failures with `try/catch`.

The preferred UI boundary is `mode: "promiseExit"` plus `Exit.isFailure`. This keeps mutation failures as values, matches Effect's error model, and prevents optimistic mutation cleanup from depending on thrown exceptions.

## Trace before changing

1. **Find the mutation setter.** Look for `const doX = useAtomSet(<mutationAtom>, { mode: "promise" })`.
2. **Confirm it is an effect-atom mutation boundary.** The setter should come from `@effect/atom-react` and a mutation atom from `./atoms`, `../api/atoms`, or plugin React atoms.
3. **Find thrown-control handling.** The same handler has `try { await doX(...) } catch (e) { ... }`, usually setting error text, resetting `adding`/`saving`, or showing a toast.
4. **Check for non-mutation async work in the same block.** If the block also awaits follow-up mutations, convert those to `promiseExit` too or keep a narrow boundary only around truly non-effect APIs.
5. **Do not rewrite unrelated local async code.** Probe requests, OAuth popup helpers, `fetch`, and browser APIs may need a different skill unless the lint finding specifically points at the mutation call.

## Fix shape

- Change the setter to `{ mode: "promiseExit" }`.
- Import `* as Exit from "effect/Exit"` if missing.
- Import `* as Option from "effect/Option"` only when extracting an optional error.
- Replace `try/catch` around the mutation with:
- `const exit = await doX(args);`
- `if (Exit.isFailure(exit)) { ...; return; }`
- success work after the failure branch.
- Use `Exit.findErrorOption(exit)` when preserving an existing error message or typed error branch.
- Keep existing typed error handling when present, e.g. `SecretInUseError`, `ConnectionInUseError`.

## Bad

```tsx
const doAdd = useAtomSet(addGraphqlSource, { mode: "promise" });

const handleAdd = async () => {
setAdding(true);
setAddError(null);
try {
await doAdd({
params: { scopeId },
payload,
reactivityKeys: sourceWriteKeys,
});
props.onComplete();
} catch (e) {
setAddError(e instanceof Error ? e.message : "Failed to add source");
setAdding(false);
}
};
```

## Good

```tsx
import * as Exit from "effect/Exit";
import * as Option from "effect/Option";

const doAdd = useAtomSet(addGraphqlSource, { mode: "promiseExit" });

const handleAdd = async () => {
setAdding(true);
setAddError(null);
const exit = await doAdd({
params: { scopeId },
payload,
reactivityKeys: sourceWriteKeys,
});
if (Exit.isFailure(exit)) {
const error = Exit.findErrorOption(exit);
setAddError(
Option.isSome(error) && error.value instanceof Error
? error.value.message
: "Failed to add source",
);
setAdding(false);
return;
}
props.onComplete();
};
```

## Follow-up mutation chains

If success work depends on the mutation result, read it after the failure branch:

```tsx
const exit = await doAdd(args);
if (Exit.isFailure(exit)) {
setAdding(false);
return;
}

const sourceId = exit.value.namespace;
```

If a follow-up effect-atom mutation can fail and the UI treats that as add failure, make that setter `promiseExit` too and branch the same way. Do not put the follow-up mutation in `try/catch` just because the first mutation now returns `Exit`.

## What not to report

- `try/catch` around non-effect APIs such as `new URL`, `JSON.parse`, raw `fetch`, or browser popup code. Those may be real lint findings, but they need a different remediation skill.
- `useAtomSet(..., { mode: "promise" })` with no local failure handling and no lint finding. Some call sites intentionally let callers decide the boundary.
- Tests or SDK/server Effect code. This skill is for React/effect-atom UI mutation handlers.
- Manual optimistic placeholder cleanup. Use `wrdn-effect-atom-optimistic` for that; if both patterns appear together, fix optimistic plumbing first, then use `promiseExit` for the remaining mutation boundary.

## Output requirements

When reviewing, report:

- **File and line** of the `useAtomSet(..., { mode: "promise" })` or `try/catch`.
- **Mutation** being called.
- **Why** it should return `Exit` at this UI boundary.
- **Fix**: the exact setter mode and the failure branch to add.

When editing, keep changes local to the handler and imports unless a follow-up mutation in the same success path must also become `promiseExit`.
30 changes: 30 additions & 0 deletions .agents/skills/wrdn-effect-schema-boundaries/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: wrdn-effect-schema-boundaries
description: Normalize unknown or loosely typed data at boundaries with Effect Schema, named guards, or typed adapters. Use when lint flags double casts, inline object assertions, unknown shape probing, or ad hoc property checks on unknown values.
allowed-tools: Read Grep Glob Bash
---

You fix one pattern: domain code is asserting or probing an unknown shape instead of parsing it once at the boundary.

## Fix Shape

- Prefer `Schema.decodeUnknownEffect(MySchema)(value)` for untrusted input.
- Keep domain code typed after the decode; do not keep `unknown` and probe it repeatedly.
- Replace `as unknown as X`, `as Record<string, unknown>`, inline object assertions, `"field" in value`, and `Reflect.get` with a schema, typed adapter, or named guard.
- A named guard is acceptable only when parsing is not the right abstraction and the guard has a precise return type.

## Good

```ts
const ParsedConfig = Schema.Struct({
endpoint: Schema.String,
});

const config = yield * Schema.decodeUnknownEffect(ParsedConfig)(raw);
```

## Bad

```ts
const config = raw as unknown as { endpoint: string };
```
107 changes: 107 additions & 0 deletions .agents/skills/wrdn-effect-schema-inferred-types/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
name: wrdn-effect-schema-inferred-types
description: Replace duplicated TypeScript shape declarations next to Effect Schema definitions with schema-derived types. Use when lint or review flags an interface/type alias that repeats fields already described by a nearby Schema.Struct, Schema.Union, Schema.TaggedStruct, or other Effect Schema model.
allowed-tools: Read Grep Glob Bash
---

You fix one pattern: a runtime `Schema` and a manual TypeScript type describe the same shape.

The preferred boundary is schema-first. Define the schema once, export `type X = typeof XSchema.Type` or `type X = Schema.Schema.Type<typeof XSchema>`, and make domain code consume the inferred type. This prevents drift between parsing and static types.

## Trace before changing

1. **Find the runtime schema.** Look for `Schema.Struct`, `Schema.Union`, `Schema.TaggedStruct`, `Schema.Record`, `Schema.Array`, or `Schema.decodeTo`.
2. **Find the duplicate static shape.** A nearby `interface X` or `type X = { ... }` repeats the same fields, nullability, optionality, or literals.
3. **Check export consumers.** If callers import the type, keep the exported type name stable and change only its definition.
4. **Confirm the schema is the source of truth.** If the manual type is wider/narrower than runtime parsing, decide whether the schema or consumers are wrong before replacing it.
5. **Handle recursion narrowly.** Recursive schemas may need one private recursive helper type to annotate `Schema.suspend`; keep exported domain types inferred from the schema.

## Fix shape

- Move the schema before the exported type alias when needed.
- Replace duplicated exported interfaces with aliases derived from the schema:

```ts
export const SourceSchema = Schema.Struct({
id: SourceId,
name: Schema.String,
enabled: Schema.Boolean,
});

export type Source = typeof SourceSchema.Type;
```

- Use `Schema.Schema.Type<typeof XSchema>` when it reads better for non-exported or generic schemas:

```ts
type IntrospectionResult = Schema.Schema.Type<typeof IntrospectionResultModel>;
```

- If using `Schema.decodeTo`, infer the domain type from the decoded/domain schema, not from the raw transport schema.
- Do not keep a manual interface solely for documentation. Add schema annotations or comments only when they clarify behavior the schema cannot express.

## Bad

```ts
export interface StoredSource {
readonly id: string;
readonly url: string;
readonly headers: readonly Header[];
}

export const StoredSourceSchema = Schema.Struct({
id: Schema.String,
url: Schema.String,
headers: Schema.Array(HeaderSchema),
});
```

## Good

```ts
export const StoredSourceSchema = Schema.Struct({
id: Schema.String,
url: Schema.String,
headers: Schema.Array(HeaderSchema),
});

export type StoredSource = typeof StoredSourceSchema.Type;
```

## Recursive schemas

Use a private helper only where TypeScript needs an annotation for self-reference:

```ts
interface TypeRefRecursive {
readonly kind: string;
readonly ofType: TypeRefRecursive | null;
}

const TypeRefSchema: Schema.Codec<TypeRefRecursive> = Schema.Struct({
kind: Schema.String,
ofType: Schema.NullOr(Schema.suspend(() => TypeRefSchema)),
});

export type TypeRef = typeof TypeRefSchema.Type;
```

The exported domain type is still schema-derived. The private helper exists only to satisfy the recursive schema definition.

## What not to report

- Domain types that intentionally do not have a runtime schema.
- Input builder types where the schema parses a different transport representation.
- Branded IDs or opaque aliases that are used by schemas but are not themselves duplicate object shapes.
- Private recursive helper types used only to type `Schema.suspend`, as long as exported consumer-facing types are inferred.

## Output requirements

When reviewing, report:

- **File and line** of the duplicated manual type.
- **Schema** that already owns the shape.
- **Why** the manual type can drift.
- **Fix**: the exact inferred alias to use.

When editing, keep exported type names stable unless every caller is updated in the same change.
Loading