AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Create

skill-ripplo-claude-plugin-create · by ripplo

Create a Ripplo end-to-end workflow from the app's typed state schema, starting-state constraints, user actions, and complete declared effects. Use when adding or extending workflow coverage.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-ripplo-claude-plugin-create

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-ripplo-claude-plugin-create)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Create? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Create a Ripplo workflow

Fix broken app behavior in the app. Strengthen incomplete workflow declarations in the workflow. Read node_modules/@ripplo/testing/DSL.md before using an unfamiliar primitive.

A workflow is a durable model of a critical user journey, not a script for one fixture. Its given must describe the widest set of starting states from which the whole journey is valid. Preserve that generality in URLs, locators, inputs, branch conditions, and effects.

Prerequisite

Run npx ripplo doctor. The app dev server and npx ripplo daemon must be running.

Inspect:

  • .ripplo/state.ts for schema-derived state handles
  • .ripplo/givens.ts, when present, and feature givens for starting-state helpers
  • .ripplo/workflows/ for local authoring patterns
  • The app component, route, mutation, and data model for the journey

State model

One root Zod schema defines all application state. Its top level contains at most one HTTP source and one browser source. Each source returns its whole validated fragment in one read.

export const state = defineState(
  z.object({
    core: source.http(z.object({
      organizations: setup.record(
        z.object({
          id: setup.generated(z.string()),
          name: setup.value(z.string()),
        }),
      ),
      projects: setup.record(
        z.object({
          id: setup.generated(z.string()),
          name: setup.value(z.string()),
          organizationId: setup.value(z.string()),
        }),
      ),
      runs: setup.record(
        z.object({
          id: setup.generated(z.string()),
          status: setup.value(z.enum(["passed", "failed"])),
          workflowId: setup.value(z.string()),
        }),
      ),
      tasks: setup.record(
        z.object({
          id: setup.generated(z.string()),
          projectId: setup.value(z.string()),
          status: setup.value(z.enum(["open", "done"])),
          title: setup.value(z.string()),
        }),
      ),
      workflows: setup.record(
        z.object({
          id: setup.generated(z.string()),
          name: setup.value(z.string()),
          projectId: setup.value(z.string()),
        }),
      ),
    })),
    frontend: source.browser(z.object({
      enabled: setup.value(z.boolean()),
      note: setup.value(z.string().optional()),
      selectedTaskId: setup.value(z.string().nullable()),
    })),
  }),
);

export const { organizations, projects, runs, tasks, workflows } = state.core;
  • setup.value() means a workflow can supply the value.
  • setup.generated() means setup generates and returns the value.
  • setup.record() means setup can create rows.
  • Plain Zod fields are observable only.
  • Wrap each source schema with source.http() or source.browser().
  • HTTP and browser sources use the same read, setup.fields, setup.records, and teardown

contract.

  • Implement source.http() in the server process and mount its framework adapter.
  • Implement source.browser() in the browser bundle and pass its engine to connect(engine)

before rendering.

  • A read-only source implements only read. A setup-capable source also implements setup and

teardown.

  • Attach createAuthenticationEngine() directly to an HTTP record handle when a workflow needs to

sign in. See /ripplo:setup for integration and ordering.

Workflow shape

One workflow is one user intent along the real click path from a natural entry point. Earlier steps create state later steps use. Set up only what the path cannot create.

const organizationActions = surface(menu("Organization actions"), { overlay: true });
const deleteOrganizationDialog = surface(dialog("Delete organization"), { overlay: true });

export const deleteOrganization = workflow("Delete an organization", () => {
  const organization = required(organizations, {});
  const remainingOrganization = optional(organizations, {});
  const project = required(projects, { organizationId: organization.id });
  const projectWorkflow = required(workflows, { projectId: project.id });
  const projectRun = required(runs, { workflowId: projectWorkflow.id });
  const organizationProjects = select(projects, {
    organizationId: organization.id,
  });
  const organizationWorkflows = select(workflows, (candidate) =>
    exists(select(organizationProjects, { id: candidate.projectId })),
  );
  const organizationRuns = select(runs, (candidate) =>
    exists(select(organizationWorkflows, { id: candidate.workflowId })),
  );

  return {
    given: [
      viewport.desktop,
      organization,
      remainingOrganization,
      project,
      projectWorkflow,
      projectRun,
      closed(organizations),
    ],
    steps: [
      goto`/organizations/${organization.id}/overview`.expect(
        visible(heading(organization.name)),
        visible(button("Organization actions")),
      ),
      click(button("Organization actions")).expect(
        visible(organizationActions),
        visible(inside(organizationActions, link("Settings"))),
      ),
      click(inside(organizationActions, link("Settings"))).expect(
        not(visible(organizationActions)),
        url.path.is`/organizations/${organization.id}/settings`,
        visible(heading("Organization settings")),
        visible(button("Show danger zone")),
      ),
      click(button("Show danger zone")).expect(visible(button("Delete organization"))),
      click(button("Delete organization")).expect(
        visible(deleteOrganizationDialog),
        visible(inside(deleteOrganizationDialog, textbox("Organization name"))),
        visible(inside(deleteOrganizationDialog, button("Delete organization"))),
      ),
      fill(
        inside(deleteOrganizationDialog, textbox("Organization name")),
        organization.name,
      ).expect(
        value(inside(deleteOrganizationDialog, textbox("Organization name")), organization.name),
        enabled(inside(deleteOrganizationDialog, button("Delete organization"))),
      ),
      click(inside(deleteOrganizationDialog, button("Delete organization"))).expect(
        not(visible(deleteOrganizationDialog)),
        deleted(organizationRuns),
        deleted(organizationWorkflows),
        deleted(organizationProjects),
        deleted(select(organizations, { id: organization.id })),
        when(
          branch("deleting the last organization")
            .if(equals(count(select(organizations, {})), exact(0)))
            .expect(url.path.is`/organizations/new`, visible(heading("Create an organization"))),
          branch("deleting one of several organizations").expect(
            url.path.is`/organizations`,
            visible(link(remainingOrganization.name)),
          ),
        ),
      ),
    ],
  };
});

Every clicked element must be declared reachable by an earlier durable visible() assertion. Every mutation step declares its user-visible result and complete application-state effect.

Before authoring, state the journey in plain language:

  • Intent: what the user is trying to accomplish
  • Entry point: where that intent naturally begins
  • Path: the real clicks and inputs, including intermediate mutations
  • Preconditions: only state required for every step to remain valid
  • Outcomes: every UI change and application-state effect, including cascades

If the description is “load this page and click this button,” zoom out until it captures the user’s actual goal. If two cases follow the same intent and click path but differ by starting state, keep them in one workflow with when. If the intent or path differs, use another workflow.

Starting state

Use the schema handles directly:

const task = required(tasks, { projectId: project.id, status: "open" });
const candidate = optional(tasks, { projectId: project.id, status: "open" });
const note = optional(state.frontend.note, arbitrary(state.frontend.note));

absent(tasks, { projectId: project.id, status: "archived" });
absent(state.frontend.note);
closed(tasks);
equals(state.frontend.selectedTaskId, task.id);
  • required() guarantees a matching row and returns typed field handles.
  • optional() lets the solver choose presence to cover branches.
  • absent() forbids a path or matching row.
  • closed() says setup created every row in that collection.
  • equals() requires a relationship or value at a path.
  • arbitrary(field) creates a typed workflow input from the field's Zod domain.
  • Omit record fields when their generated values are not shared. Use the returned record field.
  • Reuse one arbitrary binding across fields only when setup must make their values equal.
  • Interpolate record fields in workflow URLs and element locators. Don't repeat exact setup values

in actions or expectations.

  • Build locator numbers from live state with an s template. Mark fixed product copy with

exact(...).

  • Record references establish setup order. Missing dependencies and cycles are compile errors.

required, optional, absent, and closed describe meaningful state shape. Presence alone can change what the UI renders, so keep a constraint when the journey depends on that shape even if no later state assertion names it.

Start broad:

  • Omit setup-capable record fields that do not affect the journey. Setup generates them from the

schema, and the returned record exposes their values.

  • Use arbitrary(field) when a workflow input may be any schema-valid value. Reuse the binding in

the browser action and the effect.

  • Use exact(value) only when that concrete string or number causes the behavior under test.

Enums, booleans, and null already have closed domains and stay bare.

  • Use a relationship such as gt, lte, or not(equals(...)) when behavior depends on a range or

change rather than one literal.

  • Add closed(record) only when the journey needs complete collection knowledge, usually for a

count or existence branch.

exact(...) is an explicit claim of behavioral significance, not an escape hatch for the compiler. Ask: would the journey still be valid with another schema-valid value? If yes, use a generated binding or a relation. If no, make the exact value’s role clear in the workflow.

Keep state-derived browser values connected to state:

const nextTitle = arbitrary(tasks.title);
const selectedTask = select(tasks, { id: task.id });

fill(textbox("Title"), nextTitle);
click(button("Save")).expect(
  text(row(nextTitle), nextTitle),
  updated(selectedTask, {
    title: transform(({ after, before }) =>
      and(equals(after, nextTitle), not(equals(after, before))),
    ),
  }),
);

Fixed application copy such as button("Save") is fine. A record name, count, amount, slug, URL segment, or other value rendered from state must come from its handle or expression. For example, use button(s\Scope ${count(scopedWorkflows)}\), not button(exact("Scope 2")).

Branch sweep

At each step ask which starting state can change the result while preserving the same user intent and click path. Model that state with optional() or another broad constraint, then cover each reachable outcome with a named branch.

const maybeOpenTask = optional(tasks, {
  projectId: project.id,
  status: "open",
});
const openTasks = select(tasks, { projectId: project.id, status: "open" });

when(
  branch("opening a project with tasks")
    .if(gt(count(openTasks), exact(0)))
    .expect(visible(list("Tasks"))),
  branch("opening an empty project").expect(text(main(), "No open tasks")),
);

A selection used by a branch condition requires closed() for every queried collection. Otherwise unknown rows could change the answer. Branches are ordered. One unconditional fallback may appear last. Nested when() calls are invalid. Branch conditions constrain synthesis directly. Do not duplicate them as fixed starting-state values.

Include maybeOpenTask and closed(tasks) in given. The optional record gives synthesis a presence choice, while closed(tasks) makes the count complete.

State effects

select() is the only record filter:

const projectTasks = select(tasks, { projectId: project.id });
const openProjectTasks = select(projectTasks, { status: "open" });

updated(openProjectTasks, { status: "done" });
deleted(projectTasks);

Use choose(locator, value) to select an option in a browser control.

Selection membership is frozen before the action. Every selected row receives the effect. A selection used by updated() or deleted() must be provably nonempty at that step. Select through required or previously created records when one row must change. Use nested selection callbacks for relational cascades.

Creation is exhaustive:

const newTitle = arbitrary(tasks.title);
const task = created(tasks, {
  projectId: project.id,
  status: "open",
  title: newTitle,
});

Supply every setup-capable field. Schema-generated fields are implicit. Use generated() when this action generates a field that the schema otherwise marks setup.value(). Use the zero-argument absent() value for an optional creation field that must be missing. The result exposes the complete observed row.

Scalar and record field transforms are relational:

transform(state.frontend.enabled, ({ after, before }) => equals(after, invert(before)));

updated(selectedTask, {
  title: transform(({ after, before }) => not(equals(after, before))),
});

Use exact relations when the next value is known. Use broader relations only when the app chooses the value. Every effect must be provably changing from the state known at that step. Compilation rejects a possible no-op. Fix the model from the behavior:

  • If the action chooses a new value, use a transform that excludes equality.
  • If an earlier state decides whether the action changes anything, cover those cases with when.
  • If the action is valid only under a real precondition, add the least-specific constraint that

proves it and preserve excluded behavior in another branch or journey.

  • Do not hardcode a convenient previous value merely to make the proof pass.

Every observed change outside declared effects is a frame failure.

For a cascade, declare every affected collection:

const projectWorkflows = select(workflows, { projectId: project.id });
const workflowRuns = select(runs, (run) =>
  exists(select(projectWorkflows, { id: run.workflowId })),
);

deleted(workflowRuns);
deleted(projectWorkflows);
deleted(select(projects, { id: project.id }));

Browser declarations

  • Assert the new URL after navigation.
  • Use accessible roles and app-owned names from source.
  • Scope duplicate names with inside().
  • Model modal, drawer, menu, tab, and accordion containers with surface().
  • Use exclusive() for mutually exclusive UI.
  • Declare both directions of stateful UI, such as Save appearing on edit and disappearing on save.
  • Before fill, choose, clear, check, or uncheck claims another outcome, declare whether

the action changes the control. Use value(...) or not(value(...)) for value actions and checked(...) or not(checked(...)) for check actions. Add branches when the outcomes differ.

  • Use ephemeral() only for step-local evidence such as a toast.
  • Pair every application-state effect with user-visible evidence.
  • Reuse state handles and expressions for state-rendered names, values, counts, and URL parts.

Authentication

actor.set(requiredActor) signs in at start or switches actors as a step. actor.anonymous starts signed out. The required record itself is actor identity. actor.is(requiredActor) and actor.is(actor.anonymous) declare explicit actor changes. Sign-in actions are self-verifying. An actor must come from an HTTP record collection with a matching authentication engine. Browser records cannot act as signed-in actors.

App-wide facts

Use fact() only for a true invariant or permission boundary that no single journey owns:

export const noCrashScreen = fact("the app never shows the crash screen").expect(

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ripplo](https://github.com/ripplo)
- **Source:** [ripplo/claude-plugin](https://github.com/ripplo/claude-plugin)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.