npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@formfactory-dev/workflows

v0.10.0

Published

Runtime SDK for Form Factory workflow scripts (agent, sandbox, worktree).

Readme

@formfactory-dev/workflows

Runtime SDK for Form Factory workflow scripts. A small agent harness delivering the five capabilities any agent-workflow substrate has to provide — durable steps, parallel work, event-driven control flow, persistent external state, structured observability — plus integrations a workflow drives (Claude Code / Codex agent providers, docker / no-sandbox providers, git worktree management) and typed composites for common orchestration patterns.

Install

pnpm add -D @formfactory-dev/workflows

Requires Node.js 24+.

ff init adds this as a dev dependency automatically. Install directly only when authoring workflows outside an ff init repo.

Usage

A workflow is a TypeScript file at .ff/workflows/<name>.ts that imports the SDK and orchestrates one or more agent runs:

import {
  claudeCode,
  createWorktree,
  docker,
  noSandbox,
} from "@formfactory-dev/workflows"

const [ticket] = process.argv.slice(2)
const wt = await createWorktree({
  branch: ticket.toLowerCase(),
  baseBranch: "main",
})

try {
  await wt.run({
    agent: claudeCode({ model: "claude-opus-4-7" }),
    sandbox: process.env.FF_WORK_SANDBOX === "1" ? docker() : noSandbox(),
    promptFile: ".ff/prompts/work.md",
    promptArgs: { TICKET: ticket },
    idleTimeoutSeconds: 600,
  })
} finally {
  await wt.cleanup()
}

Run it: pnpm run work PROJ-1 (ff init writes the script entry). For the scaffolding flow see @formfactory-dev/cli's README.

Everything is exported from the package root — import { ... } from "@formfactory-dev/workflows". GitHub helpers (listMyOpenPrs, fetchUnresolvedThreads, etc.) live in @formfactory-dev/toolkit.

API reference

run(options)

Render a prompt, spawn an agent, watch the idle timer, capture the JSONL session into ~/.claude/projects/<encoded-cwd>/<id>.jsonl so claude --resume <id> works.

type RunOptions = {
  agent: AgentProvider // claudeCode({ ... })
  sandbox?: SandboxProvider // docker() | noSandbox(); default noSandbox()
  cwd?: string // default process.cwd()
  repoRoot?: string // for sandbox env forwarding; default = cwd
  prompt?: string // inline (no substitution)
  promptFile?: string // file with {{KEY}} placeholders
  promptArgs?: Record<string, string | number | boolean>
  idleTimeoutSeconds?: number // kill after N seconds with no output
  signal?: AbortSignal
  dryRun?: boolean // render argv without spawning
  name?: string // labels stderr lines as `[name] ...` and is
  // forwarded as the third arg to onText/onToolCall
  onText?: (text: string, name?: string) => void
  onToolCall?: (toolName: string, argSummary: string, name?: string) => void
}

Returns either { kind: "dry-run", prompt, argv, stdin? } or { kind: "ran", exitCode, session?, stdout, commits, usage? }.

  • stdout: string — assembled prose output. The agent's final result event wins when seen; otherwise the runner concatenates streamed text events. Workflows that need to parse structured output (e.g. a planner emitting JSON wrapped in <plan>...</plan>) read this.
  • commits: { sha: string }[] — commits added to cwd's HEAD during the run, oldest first.
  • usage?: TokenUsage{ inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } from the last assistant event in the session JSONL. Undefined when the session wasn't captured.

run() throws tagged errors on failure (instanceof-checkable):

  • IdleTimeoutError — no agent output for idleTimeoutSeconds. A warning: agent idle for N minute(s) line is written to stderr each minute before the kill (suppressed when onText/onToolCall callbacks are set).
  • AgentExitError — non-zero exit code; carries .exitCode.
  • PromptResolutionError — bad options (both/neither of prompt/promptFile, missing prompt file, non-positive idleTimeoutSeconds).
  • The AbortSignal's reason propagates verbatim on abort — run() does not wrap it.

interactive(options)

TUI takeover — hands stdin/stdout/stderr to the agent. Defaults to noSandbox() for the common "ask Claude something quick on this checkout" case.

createWorktree(options)

Provision a git worktree and get back a handle for chained runs:

const wt = await createWorktree({
  branch: "proj-1",
  baseBranch: "main",
  copyToWorktree: ["node_modules"],   // optional; staged before any agent run
})
try {
  await wt.run({ agent: claudeCode(...), sandbox: docker(), promptFile: "..." })
  await wt.run({ ... })
} finally {
  await wt.cleanup()
}

The handle exposes path, branch, reused, run(opts), interactive(opts), cleanup(). reused is true when an existing worktree was attached to instead of created.

claudeCode(options)

claudeCode({
  model?: string              // --model flag
  maxTurns?: number           // --max-turns
  binPath?: string            // default "claude" (PATH lookup)
})

The runner pipes the rendered prompt to the agent's stdin instead of passing it as a -p value — Claude's CLI (like all commander-style parsers) rejects values starting with -/--/---, which YAML-frontmatter prompts do.

codex(options)

Codex CLI factory, at full feature parity with claudeCode():

import { codex, run } from "@formfactory-dev/workflows"

const result = await run({
  agent: codex({ model: "gpt-5-codex", effort: "high" }),
  prompt: "do the thing",
})
// result.session?.id      — the Codex thread id (from `thread.started`)
// result.session?.jsonlPath — captured JSONL under ~/.cache/ff-cli/sessions/codex/
// result.usage            — token totals summed across all turn.completed events

Both agents share the same AgentProvider shape (AgentLauncher + AgentEventSource) so workflows pick either per run() call. The seams that differ are documented honestly:

  • Session capture lands in different places. Claude Code writes to ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl so the operator can claude --resume <id> on the host. Codex writes to ~/.cache/ff-cli/sessions/codex/<encoded-cwd>/<threadId>.jsonl because Codex's own resume mechanism (codex exec resume <threadId>) reads from its internal session store (CODEX_HOME, default ~/.codex/), not from our captured file. Our Codex JSONL is purely an observability artifact.
  • Resume invocation. For Claude, claude --resume <id> "just works" once result.session is populated. For Codex, re-spawn with ["exec", "resume", <threadId>] (a separate run() call with a custom argv builder, or a future helper).

detectRepo(options?)

detectRepo({
  cwd?: string                // default process.cwd()
  remote?: string             // default "origin"
})

owner/repo slug from the named git remote, falling back to the cwd basename when the remote is missing or unparseable. Handy for {{REPO}} in prompt templates.

docker(options) / noSandbox()

docker({
  image?: string              // when omitted, lazily build from .ff/sandbox/Dockerfile
  network?: string            // default = docker bridge
})

When options.image is omitted, docker() builds an image from <repoRoot>/.ff/sandbox/Dockerfile on first use and tags it ff-workflow-sandbox:<sha12> (sha of the full Docker build context — Dockerfile plus every sibling file it could COPY/ADD). Edits to any context file produce a new tag and a fresh build automatically.

The policy is locked down inside the SDK (non-root user, all caps dropped, no new privileges; bind-mount matches host path so claude --resume works on the host).

GitHub helpers

Live in @formfactory-dev/toolkit. All shell out to gh / gh api graphql, reusing the operator's existing auth.

import {
  listMyOpenPrs,
  fetchUnresolvedThreads,
  replyToThread,
  resolveThread,
  addReaction,
} from "@formfactory-dev/toolkit"

const prs = await listMyOpenPrs()
const threads = await fetchUnresolvedThreads(prs[0].number)
await replyToThread(threads[0].id, "Addressed in <sha>: <one-liner>")
await addReaction(threads[0].comments[0].id, "+1")
// Optional — `revise.md` defaults to leaving threads open as audit trail.
await resolveThread(threads[0].id)

These power the bundled revise.ts workflow. gh calls bound to a 30 s timeout; reviewThreads paginate; listMyOpenPrs sets --limit 1000 (gh defaults to 30).

Harness capabilities

The SDK is a small agent harness: it delivers the five capabilities any agent-workflow substrate has to provide, plus the integrations a workflow drives (agents, sandboxes, worktrees). Higher-level patterns (review loops, panels, optimisers, fan-out fan-in) are exported as composites — typed callable functions you import from this package — see the Composites section below.

| Capability | Modules | What it gives you | | ------------------------- | ------------------------- | --------------------------------------------------------------------------- | | Durable steps | step, run | Named, replayable units of work. Rerunning a workflow skips completed work. | | Parallel work | parallel | Structured concurrency: settle / fail-fast / cancel-siblings. | | Event-driven control flow | signal, timer, saga | Typed rendezvous, journaled sleep, LIFO compensation. | | Persistent external state | journal | Append-only log of step outcomes, signals, run lifecycle. | | Structured observability | recorder | Observation seam — stderr printer, JSONL capture, usage rollup. |

The architectural rationale lives in docs/adr/. The 3-layer architecture (primitives / composites / workflows) is described in docs/architecture.md. The vocabulary is in GLOSSARY.md.

A workflow with durable steps

import {
  claudeCode,
  createRunContext,
  defaultRecorder,
  jsonlJournal,
  newRunId,
  parallel,
  run,
  step,
  withRunContext,
} from "@formfactory-dev/workflows"

const runId = process.env.FF_RUN_ID?.trim() || newRunId()
const ctx = createRunContext({
  runId,
  recorder: defaultRecorder(),
  journal: jsonlJournal({ runId }),
})

await withRunContext(ctx, async () => {
  await parallel(
    tickets,
    (ticket) =>
      step(`work ${ticket}`, async () => {
        const plan = await step(`plan ${ticket}`, () => planTicket(ticket))
        for (const subtask of plan) {
          await step(`implement ${subtask.branch}`, () =>
            implementSubtask(ticket, subtask),
          )
        }
      }),
    { concurrency: 4, policy: "settle" },
  )
})

Rerun with FF_RUN_ID=<id> to resume — completed steps are skipped and their recorded output is replayed. Step outputs must be JSON-serialisable (plain objects, arrays, primitives).

step(name, body, options?)

async function step<T>(
  name: string,
  body: (ctx: RunContext) => Promise<T> | T,
  options?: {
    ctx?: RunContext // override ambient withRunContext scope
    idempotencyKey?: string // override the name-derived stepId
    retryable?: (err: unknown) => boolean
  },
): Promise<T>

Identifies steps by parentStepId/name (or idempotencyKey). Duplicate step names at the same level throw — disambiguate by naming or include ticket/index data in the name. Failures record step_failed rows; on rerun, non-retryable failures throw ResumedStepFailure without invoking the body. Mark transient errors with retryable to allow re-invocation.

parallel(items, worker, options)

function parallel<T>(
  items: T[],
  worker: (item: T, signal: AbortSignal) => Promise<void> | void,
  options: {
    concurrency: number
    policy?: "settle" | "fail-fast" | "cancel-siblings" // default "settle"
    signal?: AbortSignal
  },
): Promise<{ ok: T[]; failed: Array<{ item: T; error: unknown }> }>

Workers receive a per-call AbortSignal derived from the parent. Under fail-fast and cancel-siblings, the first failure aborts that signal so in-flight workers can bail with cleanup. fail-fast rejects the parent promise with the first error; cancel-siblings resolves with the partial result. The legacy (item) => Promise<void> worker shape is still accepted.

signal(name) / emit(ref, value) / waitFor(ref)

const approval = signal<{ approved: boolean }>("approve")
await emit(approval, { approved: true }) // writes signal_emitted
const decision = await waitFor(approval, { timeoutMs: 60_000 })

Broadcast semantics: every waitFor matching (name, correlation) resolves on the latest emission, so fan-out patterns work out of the box. Sequential queue semantics use distinct correlation per task. Polling interval defaults to 200ms; the polling loop respects ctx.signal and the provided timeoutMs.

timer({ id, ms, signal? })

Journaled sleep. On first call records timer_started, sleeps ms, records timer_done. On resume with the same runId + id: a completed timer returns immediately; an interrupted one (started but not done) waits the remainder (max(0, startedAt + ms - now)).

await timer({ id: "rate-limit-cooldown", ms: 30_000 })

Abort via signal. The abort's reason rejects the call and timer_done is not recorded — the next resume continues waiting from the original startedAt. ms must be a positive finite number; id must be non-empty.

saga({ id }, body)

Structured compensation. The body receives a ctx.onCompensate(fn) hook; on body failure the registered compensations run LIFO before the original error re-throws. The outer saga is journaled as one step(), so a completed saga skips the body on resume. Compensations themselves are NOT individually journaled — wrap them in step() if you need durability.

await saga({ id: "deploy" }, async (ctx) => {
  await step("provision", () => provision())
  ctx.onCompensate(() => step("rollback-provision", () => deprovision()))
  await step("publish", () => publish())
  ctx.onCompensate(() => step("rollback-publish", () => unpublish()))
})

If a compensation throws, remaining compensations still run and the collected errors are attached to a SagaCompensationError with originalError + compensationErrors[] properties.

Journal / Recorder / RunContext

const ctx = createRunContext({
  runId: newRunId(),
  recorder: defaultRecorder({ name: "myflow" }),
  journal: jsonlJournal({ runId }), // or inMemoryJournal() for tests
})
await withRunContext(ctx, async () => {
  /* ... */
})
  • Journal lives at ~/.cache/ff-cli/runs/<runId>.jsonl by default. Pass dir to override.
  • Recorder defaults to stderr printing + JSONL capture + token-usage rollup. silentRecorder() is available for tests; defaultRecorder() accepts the same onText / onToolCall hooks the legacy RunOptions did.
  • RunContext propagates via AsyncLocalStoragestep(), signal(), emit(), waitFor() pick it up automatically inside withRunContext.

Composites

Higher-level orchestration patterns ship as composites — typed callable functions that compose primitives. Import directly from this package:

import {
  kanban,
  reviewLoop,
  gatherAndSynthesize,
} from "@formfactory-dev/workflows"

Each composite encodes a pedagogical pattern (when to use, when not to) in its JSDoc and exposes a typed opts/result shape. Composites live under packages/workflows/src/composites/<category>/; categories match the functional cut in GLOSSARY.md § Composites, by function.

| Composite | Shape | | --------------------- | --------------------------------------------------------------------- | | approvalGate | risk assess → optional waitFor approval → execute | | checkSuite | N checks in parallel → verdict agent aggregates | | classifyAndRoute | classifier labels items → specialists handle each in parallel | | contentPipeline | sequential refinement stages (outline → draft → edit) | | debate | proposer/opponent alternate for N rounds → judge verdict | | decisionTable | classifier → flat rule table (first-match / all-match) | | driftDetector | capture → compare to baseline → alert when drifted | | escalationChain | sequential tiers with confidence thresholds → optional human fallback | | gatherAndSynthesize | N source-specific gathers in parallel → synthesiser | | kanban | items walk ordered columns; each (item, column) is its own step | | mergeQueue | sequentially merge a list of branches with skip-on-conflict | | optimizer | generator/evaluator loop; returns best-seen candidate | | panel | role-specific reviewers run in parallel → moderator synthesis | | poller | repeatedly check a condition until satisfied | | ralph | loop one agent on a freeform prompt until stopped or maxIterations | | reviewLoop | producer/reviewer iteration until approved | | runbook | sequential ops with per-step risk gating via approval signals | | scanFixVerify | scan → parallel fix → verify; retry until clean | | supervisor | boss plans → workers run in parallel → boss re-delegates failures |

The architectural rationale and full layering model are in docs/architecture.md.

Prompt template substitution

When promptFile is set, {{KEY}} placeholders are replaced with values from promptArgs. Inline prompt: is passed through verbatim — combining promptArgs with inline prompt is rejected.

Live progress

By default the runner mirrors Claude's text to stderr and prints → <ToolName> <preview> per tool call (per-tool display field: Bash → command, Read → file_path, etc.; truncated at 200 chars).

Set RunOptions.name to prefix every line [name] … for parallel runs sharing a terminal. The value is sanitised (ANSI + control characters stripped), so name is safe to derive from external strings like Jira ticket titles.

Pass onText / onToolCall to take over rendering. Either hook suppresses the default printer and receives name as a third argument. JSONL capture to ~/.claude/projects/... runs regardless.

Sandbox auth

Create <repoRoot>/.ff/.env (gitignored) with either:

CLAUDE_CODE_OAUTH_TOKEN=   # from `claude setup-token` — reuses your subscription
ANTHROPIC_API_KEY=         # pay-as-you-go alternative

Empty values fall through to process.env. Keys present in the file form the allowlist of vars forwarded into the sandbox.

Publishing

Published via pnpm publish from GitHub Actions using OIDC (the npm scope is configured as a "trusted publisher" tied to this repo and the release workflow — local publishes are rejected). pnpm publish rewrites catalog: and workspace: protocol references in package.json to concrete version ranges before upload, so consumers installing via npm, yarn, pnpm, or bun see normal version strings.

License

MIT — see LICENSE.