--- url: https://salimhamed.github.io/jigs/guide/why-jigs.md --- # Why jigs Coding agents are capable but not repeatable. Ask one to take a ticket to a merged pull request twice and it may take two different routes, skip a check, or forget where it stopped when the session ends. The process lives in a prompt and in the agent's memory, and both drift. A workflow writes that process down as code. Every run follows the same steps, in the same order, with the agent doing only the parts that need judgment. The run waits for a person, a review or a CI build without losing its place, and it leaves a record of what happened. That is what jigs gives you: the flexibility of agents inside a process you can trust and rerun. ## Built on the Vercel Workflow SDK jigs runs on the [Vercel Workflow SDK](https://useworkflow.dev). A workflow is an async function marked `"use workflow"`; a step is a function marked `"use step"`. The SDK records each finished step, so a run that pauses or restarts resumes where it was and never repeats completed work. A workflow is ordinary TypeScript. It can call jigs' routines and steps, such as `runAgent` or `openPullRequest`, but it does not have to: any code and any library can go in it, within the SDK's rules for workflows and steps. Your workflows live in a **factory repo**, a repository of your own that installs jigs as a package. The factory runs its own service on your machine, with its own Postgres database and a dashboard of every run. ## Features * **Several repositories per factory**, each run in its own Git worktree. * **Linear tickets and human questions**: claim a ticket, ask on it, pause for the reply. * **Claude Code, Codex and Pi harnesses**, or a model API called directly. * **Your existing Claude and Codex subscriptions** pay for agent work. * **Optional webhooks**: waiting runs poll GitHub and Linear without them. --- --- url: https://salimhamed.github.io/jigs/guide/getting-started.md --- # Install and run a first workflow This guide creates a factory, starts its service and runs `hello`, the workflow every new factory includes. `hello` calls no model and changes no repository, so you need no credentials to try it. ## Set up with your agent If you use a coding agent, you can let it do the steps below. Install the jigs skill, which adds a single skill named `/jigs`: ```sh npx skills add salimhamed/jigs ``` Then ask your agent `/jigs set up a factory in this empty directory`. The rest of this page is the same process by hand. ## 1. Host dependencies * **Node.js 24 or newer** and **pnpm**. * **Docker**, with its daemon running. Each factory runs its own Postgres container. * **Agent CLIs, only for the harnesses your workflows use.** `hello` uses none. Install each one yourself, keep it on the `PATH` of the shell that starts the service, and log in: | Harness | Command | Log in | | --- | --- | --- | | Claude Code | `claude` | `claude auth login` | | Codex | `codex` | `codex login` | | Pi | `pi` | run `pi`, then `/login` | * **On Linux**, run `loginctl enable-linger "$USER"` once, so the service keeps running after you log out. On a host without systemd, such as macOS, the service runs unsupervised and stops when you log out. jigs installs from public npm as `@jigs-ai/jigs`. Each factory pins its own version, so there is nothing to install globally and no registry token. ## 2. Create a factory ```sh mkdir my-factory cd my-factory git init pnpm --config.minimum-release-age-exclude=@jigs-ai/jigs dlx @jigs-ai/jigs init ``` pnpm holds back packages published in the last day. The flag lets you get the newest jigs, and it applies only to jigs. `init` writes the starting files and prints the next steps. It does not start anything. `workflows/hello/hello.ts` is the first workflow, and `jigs.config.ts` registers it under the name `hello`. ## 3. Start the service ```sh pnpm install cp .env.example .env pnpm exec jigs up ``` `pnpm install` puts this factory's jigs in place for `pnpm exec`. `hello` needs nothing filled in `.env`. `jigs up` starts Postgres, builds the factory, starts the service and waits until it is ready. It then runs `jigs doctor`, which checks only what your workflows use; rerun it any time with `pnpm exec jigs doctor`. It ends by naming the two things it runs, one Postgres container and one service process that also serves the dashboard, and the one command that stops both: ``` my-factory is up postgres localhost:5440 (Docker container my-factory-postgres-1) service http://localhost:8990 (pid 53812) dashboard http://localhost:9090 logs ~/.local/share/jigs/services/my-factory-2286ac2a.log stop: pnpm exec jigs down ``` Open the dashboard URL from your own output. It shows every run and its steps. `pnpm exec jigs service stop` stops the service and its dashboard and leaves Postgres running; `pnpm exec jigs down` stops both and keeps Postgres's data. ## 4. Run hello ```sh pnpm exec jigs run hello pnpm exec jigs status ``` `run` prints the new run's ID and its dashboard link. `status` lists runs; pass a run ID to see one run in detail: ```sh pnpm exec jigs status ``` The run should finish as completed. From here, write your own workflow with [Build a workflow](/guide/build-a-workflow). Binding a repository needs GitHub credentials, so set them first: see [GitHub identity](/guide/configuration#github-identity). --- --- url: https://salimhamed.github.io/jigs/guide/concepts.md --- # Core concepts A factory has two kinds of function. A workflow function decides what happens next. A step function does the work. They run in different places, and the line between them is the one hard rule in jigs. This page starts with the words, then the rule, then what follows from it. | Term | Meaning | | --- | --- | | Factory | Your repository of workflows and configuration. It runs its own service. | | Workflow | A [Vercel Workflow SDK](https://useworkflow.dev) workflow: an async function whose first statement is `"use workflow"`. It decides what happens next. | | Step | One durable, recorded operation: a function marked `"use step"`. It runs in a worker and does the work. | | Routine | A function you call from a workflow. It runs steps and can wait for something outside the run. | | Harness | An agent program jigs starts, such as Claude Code, Codex or Pi. It works with tools in a directory. | | Agent | A named harness configuration playing a part in your workflow, such as a builder or a reviewer. | | Agent session | One agent across several turns of a workflow, so each turn remembers the ones before it. | | Session reference | The small piece of data a later step uses to resume the same harness session. | | Run | One execution of a workflow, including any time it spends waiting. | | Suspension | A run waiting on something outside it, such as a reply on a ticket or a pull-request review. | | Wake | A nudge that makes a suspended run check its condition again. It does not answer the wait for you. | | Binding | A name for a target repository, mapped to its remote URL. | | Worktree | A Git working directory jigs cuts from its clone of a binding, one per run and branch. | | Recipe | A complete workflow you copy into your factory and then own. | | Model source | A model API that answers one request directly, such as OpenRouter. | | Preflight | The checks jigs runs on a workflow's `requires` before it creates a run. A failure names its repair, and no run starts. | | Jev | A decision call, `askJev`, that returns calibrated probabilities for named yes/no, choice or score questions. | ## The boundary A step call is a message through the database, not a function call. When a workflow awaits a step, five things happen: 1. The workflow writes the argument to the database as JSON, and pauses. 2. A worker reads the argument, possibly in another process, possibly after a restart. 3. The worker runs the step's body: files, network, agents. 4. The worker writes the result to the database as JSON. 5. The workflow replays from its first line, and this time the `await` returns the stored result. So an argument or a result has to be something that can be written down: strings, numbers, booleans, arrays and plain objects. A function has no written form, and neither does an object that holds a running process. A harness descriptor crosses the boundary, because it is only data. A provider object does not: its methods are functions, and it holds a child process. ```ts // Crosses: it is only data. harnesses.claude({ model: "opus", effort: "high" }); // => { kind: "claude", model: "opus", effort: "high" } // Does not: a provider from the AI SDK is made of functions. claudeCode("opus"); // SerializationError: Failed to serialize step arguments at path "..." ``` That is why a workflow sends a description and the step builds the live thing from it. The same rule explains why prompts are rendered to strings before a step call, why a session reference is a small piece of data, and why your factory holds a generated step file at all. ## Workflows decide, steps do the work A workflow is replayed from its first line every time the run wakes. Steps that already finished return their recorded result instead of running again. ```ts export async function hello(_input: WorkflowInputs) { "use workflow"; return await createRunDirectory(); } ``` So a workflow, and every routine it calls, must be safe to run again. It reads no files, calls no network, runs no Git commands and reads no `process.env`. The workflow bundle has no Node built-ins, so an import that needs one fails the build. That work belongs in steps. jigs provides steps for the common operations, and you can write your own in a `steps.ts` beside the workflow that uses it. ## Routines compose steps A routine is a function you call from a workflow. It calls steps for you and can wait for something outside the run, such as a reply on a ticket. ```ts import { reviewTicket } from "#jigs/routines"; const handoff = await reviewTicket({ claim, snapshot, harness: agents.reviewer, cwd }); ``` A routine has no directive and no recorded result of its own. Only the steps it calls are recorded, so a routine can change between jigs releases without stranding a waiting run. ## Agents are named harnesses An agent is a harness configuration with a name: the part it plays in your workflow. A workflow lists its agents in a plain object and declares them in `requires`. ```ts import { defineWorkflow, harnesses } from "@jigs-ai/jigs"; const agents = { builder: harnesses.claude({ model: "opus", effort: "high" }), reviewer: harnesses.codex({ model: "gpt-5.6-sol" }), }; export default defineWorkflow({ inputs, requires: { agents, integrations: ["linear", "github"] }, workflow: shipTicket, }); ``` The service checks each agent's harness when it starts, and preflight checks it before every run. A run chooses among the names; to change a model, edit its line in `agents`. An agent session is one agent across several turns of a workflow: it resumes the harness session it holds, and starts fresh when it cannot. A session reference is the piece of data that makes the resume possible, small and plain enough to pass to a later step. ## The factory holds generated code The Workflow SDK gives each workflow and step a durable ID made from its file path and function name. A waiting run is tied to those IDs. `jigs generate` writes two files into `jigs/`, and a workflow imports from them by name: ```ts import { provisionWorktree, setTicketStatus } from "#jigs/steps"; import { reviewTicket, runAgent } from "#jigs/routines"; ``` `jigs/steps.ts` holds every step jigs provides: small `"use step"` wrappers around jigs' operations. It is the only generated file with a directive, so every name in it is recorded and replayed. `jigs/routines.ts` holds the routines, bound to those steps. `runAgent`, `reviewTicket`, `watchPullRequest` and `pullRequestGate` are routines. Commit `jigs/` and never edit it. `jigs generate` refreshes it from the installed jigs version, a build fails with that repair if it is out of date, and `jigs upgrade` regenerates it for you. The directives live in your factory because the IDs have to stay put. If the `"use step"` functions lived inside the jigs package, every upgrade would move them and strand the runs that were waiting. ## Renaming moves an ID Moving or renaming a workflow file, a workflow function or a step changes its address. Finish or cancel the runs that use it before you deploy the rename. The same holds for the generated files: a jigs release that moves `jigs/steps.ts` or renames a step in it moves those IDs, and says so in its release notes. ## Two import sources A workflow imports from two kinds of place. The library, `@jigs-ai/jigs`, holds types, constructors and pure functions such as `defineWorkflow`, `harnesses` and `renderTicketSnapshot`. `#jigs/steps` and `#jigs/routines` hold the durable operations generated for your factory. ```ts import { defineWorkflow, harnesses } from "@jigs-ai/jigs"; import { reviewTicket } from "#jigs/routines"; import { provisionWorktree, setTicketStatus } from "#jigs/steps"; import { implementAndReview } from "./delivery/delivery.ts"; ``` Import a workflow's own files with relative paths. The workflow loaders in `jigs.config.ts` stay relative too. The split is there so you can see it. Every name from `#jigs/steps` is one recorded operation, and every name from `#jigs/routines` composes those. ## Factory layout `jigs init` writes these files: | File | What it is for | | --- | --- | | `jigs.config.ts` | Service ports, bindings, registered workflows, schedules and policy. See [Configuration](/guide/configuration). | | `jigs.config.test.ts` | Checks the registered workflows and the durable IDs the build emits. | | `jigs/steps.ts`, `jigs/routines.ts` | The generated files described above. | | `workflows/hello/hello.ts` | The first workflow. Each workflow gets its own directory under `workflows/`. | | `.env.example` | The environment file template. Copy it to `.env` for secrets. | | `package.json` | Pins jigs and maps `#jigs/*` to the files in `jigs/`. | | `nitro.config.ts`, `docker-compose.yml`, `tsconfig.json`, `vitest.config.ts`, `pnpm-workspace.yaml`, `.gitignore`, `README.md` | Build, database and tooling settings. | A workflow keeps what it owns beside it. Its own `"use step"` functions go in a `steps.ts` in its directory, and its prompts and other files next to that. --- --- url: https://salimhamed.github.io/jigs/guide/build-a-workflow.md --- # Build a workflow This page builds one workflow from start to finish: `triage` takes a bug report, has an agent investigate it in a repository, and asks a model to turn the findings into a structured verdict. It assumes a running factory from [Install and run a first workflow](/guide/getting-started). ## 1. Connect a repository The agent needs a repository to work in. Binding one needs GitHub credentials, so set them first: see [GitHub identity](/guide/configuration#github-identity). Then bind the repository and bring the factory up so the service clones it: ```sh pnpm exec jigs bind git@github.com:owner/app.git pnpm exec jigs up ``` The binding's name comes from the repository name, here `app`. A workflow that needs no repository can skip this and use `createRunDirectory()` from `#jigs/steps` for a scratch directory instead, as `hello` does. ## 2. Write the workflow Create `workflows/triage/triage.ts`: ```ts import { defineWorkflow, harnesses, JigsError, models, type WorkflowInputs } from "@jigs-ai/jigs"; import { z } from "zod"; import { askModel, runAgent } from "#jigs/routines"; import { provisionWorktree } from "#jigs/steps"; const inputs = z.object({ binding: z.string().default("app"), report: z.string().min(1), }); const agents = { investigator: harnesses.claude({ model: "sonnet" }) }; const summarizer = models.openrouter("google/gemini-2.5-flash-lite"); const verdict = z.object({ reproducible: z.boolean(), severity: z.enum(["low", "medium", "high"]), summary: z.string(), }); export async function triage(input: WorkflowInputs) { "use workflow"; const worktree = await provisionWorktree({ binding: input.binding, branch: `triage/${input.triggerId}`, }); const investigation = await runAgent({ harness: agents.investigator, cwd: worktree.path, prompt: `Investigate this bug report. Try to reproduce it and find the cause. Do not change files.\n\n${input.report}`, }); const result = await askModel({ model: summarizer, prompt: `Turn these findings into a triage verdict:\n\n${investigation.text}`, output: verdict, }); if (!result.output.reproducible) { throw new JigsError( "the agent could not reproduce the report", "add steps to reproduce to the report and run triage again", ); } return result.output; } export default defineWorkflow({ inputs, requires: { agents, bindings: ["app"], models: [summarizer] }, workflow: triage, }); ``` What each part does: * **`inputs`** is a zod schema. `jigs run` checks `--input` values against it before a run is created. jigs also adds `triggerId`, an ID unique to the run, which here gives each run its own branch. * **`"use workflow"`** marks the function as a durable workflow. Its body must be safe to replay, so all real work happens in the steps it calls. * **`agents`** names each agent the workflow runs, by the part it plays. A harness descriptor is plain data, so it can be passed to a step. * **`provisionWorktree`** cuts a worktree for this run from the binding's clone, on the branch you name. A resumed run gets the same worktree back. * **`runAgent`** runs an agent, here Claude Code, in that directory with its tools. `result.text` is its final answer. Pass `output` a zod schema to get a parsed `result.output` instead. * **`askModel`** calls a model API directly, with no tools and no directory. It suits summarizing and classifying text you already have. The `output` schema checks the answer's shape, not whether it is right. * **`JigsError`** ends the run as failed, with a hint for whoever reads it. Returning a value always means success. * **`defineWorkflow`** ties the function, its inputs and its requirements together, and makes TypeScript check the function's parameter against the schema. It is the file's default export. * **`requires`** lists what the workflow needs: its agents, the binding and the model source. jigs derives the harness CLIs to check from the agents. Preflight checks each one before every run and refuses to start with a repair when one is missing. `jigs doctor` runs the same checks. This model source reads `OPENROUTER_API_KEY` from the factory's `.env`. See [Models and harnesses](/guide/models-and-harnesses) for the other harnesses and sources and what each one needs. ## 3. Register it Add the workflow to the `workflows` map in `jigs.config.ts`: ```ts workflows: { hello: () => import("./workflows/hello/hello.ts"), triage: () => import("./workflows/triage/triage.ts"), }, ``` The import stays deferred, so commands that only read configuration never load workflow code. ## 4. Run it ```sh pnpm exec jigs up pnpm exec jigs run triage --input report="Saving a draft twice loses the title." pnpm exec jigs watch ``` `jigs up` rebuilds the factory and restarts the service because the workflow changed. `watch` follows the run step by step; `jigs status ` shows the result when it finishes. On success the worktree is released automatically. See [`release`](/guide/configuration#release) to keep it instead. ## Ask a person and wait `haltForHuman` posts a question on a Linear ticket and suspends the run until someone replies there. It needs Linear credentials (see [Linear identity](/guide/configuration#linear-identity)) and a claimed ticket. Claiming also makes sure only one run works on a ticket at a time: ```ts import { claimTicket, haltForHuman } from "#jigs/routines"; import { resolveLinearIssue } from "#jigs/steps"; const issue = await resolveLinearIssue(input.ticket); const claim = await claimTicket(issue.id, issue.identifier); const reply = await haltForHuman(claim, { headline: "Triage needs a decision before it continues.", where: "triage", questions: [ { question: "Should the fix include archived drafts?", options: [{ label: "Active drafts only" }, { label: "Include archived drafts" }], }, ], onReply: "continue", }); // reply.body is the person's answer, as free text. ``` Add `ticket: z.string()` to `inputs` and `integrations: ["linear"]` to `requires`. `jigs status ` shows the question and the link to answer it. The run notices a reply on its next [check](/guide/configuration#webhooks); `jigs poke ` checks now. Answer the existing run rather than starting another one. ## Wait on a pull request `watchPullRequest` yields the current GitHub facts immediately, then yields again when those facts change. The workflow decides what to do with them: run an agent, apply rules, or keep waiting. The watcher makes no model calls and does not decide whether a comment needs an answer. This example continues a builder session created earlier in the workflow. `pr` identifies the pull request by `owner`, `repo` and `number`: ```ts import { watchPullRequest } from "#jigs/routines"; for await (const snapshot of watchPullRequest(pr)) { if (snapshot.state === "closed") return { merged: snapshot.merged }; const situation = JSON.stringify(snapshot); await builder.run({ resume: `Read this PR's discussion and checks. Address anything that needs attention, or do nothing if it is already handled. You may respond on GitHub and push fixes. Do not merge. Current facts: ${situation}`, fresh: `${task} Continue work on PR #${pr.number} in ${pr.owner}/${pr.repo}. Read the code and discussion, then respond or push fixes if needed. Do not merge. Current facts: ${situation}`, }); } ``` The example's `task` and `builder` belong to the factory. It shows one agent invocation per update; see the [recipe](/guide/recipes#linear-ticket-to-pr) for checking the agent's work and retrying incomplete local changes within a factory-owned limit. See [agent sessions](/guide/models-and-harnesses#agent-sessions) for creating the builder and supplying recovery context in `fresh`. `snapshot.state` and `snapshot.merged` come from GitHub. The snapshot also includes `headSha`, draft and merge state, labels, reviews, inline review threads and conversation comments. jigs summarizes GitHub checks and commit statuses as `ci` (`"red"`, `"green"` or `"pending"`) and includes `failingChecks`. These are observed facts, not an assessment that the work is finished. To compare a fresh read with an earlier snapshot, import `pullRequestSnapshotKey` from `@jigs-ai/jigs` and compare their keys. It uses the same fact comparison as the watcher, ignoring collection ordering and incidental fetch metadata. Repeated notifications with unchanged facts produce no new snapshot. An agent's own comments and pushes do change the facts and can produce another turn. An agent invocation that decides nothing needs doing is normal. The watcher has no hidden agent budget or conversation filter. The recipe bounds recovery attempts for each update, rather than limiting the total number of updates a PR can receive. Agents may post using their GitHub tools: no hidden jigs marker is required, and an unmarked comment does not automatically mean unresolved work. The watcher yields a closed snapshot once, then ends. Leaving the loop by `return`, `break` or a throw releases the watch. It shares the existing PR hook with `pullRequestGate`: only one run can watch a given pull request at a time. A second owner receives a claim conflict. The service's polling, webhooks and `jigs poke` wake the watch to reread GitHub. The watcher never merges. Factory code decides who may merge and calls `mergePullRequest` when appropriate; that step rechecks current GitHub facts and the [merge approval policy](/guide/configuration#merge). The [linear-ticket-to-pr recipe](/guide/recipes#linear-ticket-to-pr) demonstrates continuing the builder session after publication with this policy. ### Use the rules-based gate `pullRequestGate` is an alternative for workflows that want jigs to classify outstanding work using its marker rules. Loop over it with `for await`: each wake says what is outstanding right now. ```ts function pullRequestGate( pr: PullRequestRef, options: { scope: string; approval: MergePolicy["approval"]; worktree?: Worktree }, ): AsyncIterable; type PullRequestWake = | { kind: "closed"; merged: boolean } | { kind: "merge-ready"; headSha: string; retryNoted: boolean } | { kind: "ci-red"; headSha: string; failing: CheckRun[]; mentionLogin: string | null } | { kind: "review-comments"; threads: ReviewThread[]; body?: string }; ``` ```ts import { postPullRequestNote, pullRequestGate } from "#jigs/routines"; import { mergePullRequest, resolveMergePolicy } from "#jigs/steps"; const merge = await resolveMergePolicy(input.binding); const scope = `triage/${input.ticket}`; const gate = pullRequestGate(pr, { scope, approval: merge.approval, worktree }); for await (const wake of gate) { if (wake.kind === "closed") return { merged: wake.merged }; if (wake.kind === "merge-ready") { const result = await mergePullRequest(pr, wake.headSha, merge); if (result.merged) return { merged: true }; await postPullRequestNote({ pr, scope, reason: result.transient ? "merge-retry" : "merge", headSha: wake.headSha, body: `I could not merge this pull request: ${result.reason}.`, }); continue; } // ci-red and review-comments: fix, push, answer the threads } ``` Leaving the loop stops watching, whether by `return`, `break` or a throw. Only one run can watch a pull request at a time, so a second gate on the same pull request fails with a claim conflict. The `scope` names this workflow's work on the pull request. Every comment jigs posts carries it in a hidden marker, and the gate reads those markers back to decide what is still outstanding. Keep the scope stable, so a later run recognises its own answers. A wake is delivered only while its head is still the pull request's head. If the branch moved while you handled an earlier wake, a red build on the old commit is dropped rather than repaired twice. Pass the `worktree` your workflow pushes from, and the gate also checks each red build and review wake against the local branch. A wake for an older commit of that branch is dropped: the run has moved past it, even if GitHub still reports it in the moment after a push. A wake for a commit the worktree does not have is delivered, because someone else pushed it and it still needs an answer. `postPullRequestNote` posts once per commit and reason, so a merge you retry on every wake reports its refusal once. ## Record what the workflow created `jigs status ` lists a run's resources, such as its worktree. Record anything else a person may need to find with `registerResource`: ```ts import { registerResource } from "#jigs/steps"; await registerResource({ kind: "s3-report", identity: "quarterly/2026-Q3", url: "https://reports.example.com/quarterly/2026-Q3", }); ``` The kind and identity together name the resource, so registering it again only updates its URL. When the thing you created cannot safely be created twice, create it in one step and register it in a separate call afterwards, so a retry repeats only the registration. A record is for finding things; it never permits jigs to delete them. ## Explore further * [Library API](/api/jigs): harnesses, models, every option of `runAgent`, `askAgent`, `askModel` and `askJev`, and the data steps hand back. * [Linear steps](/api/steps/linear): ticket notes, questions and snapshots. * [Runtime steps](/api/steps/runtime): run directories, resources and release. * [Workspace steps](/api/steps/workspaces): worktrees. * [Git steps](/api/steps/git): reading a change and its patch. * [Pull request steps](/api/steps/pull-requests): opening, reviewing and merging. * [Models and harnesses](/guide/models-and-harnesses): what each harness and model source needs. --- --- url: https://salimhamed.github.io/jigs/guide/models-and-harnesses.md --- # Models and harnesses A **harness** is an agent program jigs starts, such as Claude Code. It runs its own agent loop and can use tools, a working directory and a session you resume later. A **model source** is an API that answers one request directly, such as OpenRouter. Both are described by plain descriptors you build in workflow code: ```ts import { harnesses, models } from "@jigs-ai/jigs"; harnesses.claude({ model: "sonnet", effort: "high" }); harnesses.pi(models.openaiCodex("gpt-5.5"), { thinking: "high" }); models.openrouter("google/gemini-2.5-flash-lite"); ``` The kinds are fixed: a factory cannot add a harness or model kind, `models.openaiCompatible` reaches any endpoint that speaks the OpenAI API, and a native Anthropic or Gemini API becomes a built-in kind when a factory needs one. ## The four verbs Import them from `#jigs/routines`. | Verb | Takes | Use it to | | --- | --- | --- | | `runAgent` | a harness and a `cwd` | Do work with tools in a directory: read code, run checks, make a change. | | `askAgent` | Claude Code or Pi, without tools | Get one answer from a harness with no tools and no directory. | | `askModel` | an OpenRouter or OpenAI-compatible source | Call a model API directly, for summaries and classification. | | `askJev` | an OpenRouter jev model | Get calibrated probabilities for named yes/no, choice or score questions. | Each verb takes an optional zod `output` schema (`askJev` returns its own typed answers). An optional field also accepts `undefined`, so you can pass a value that may be missing without a conditional spread. Name each agent a workflow runs in its `requires.agents`, and add each model source it calls to `requires.models`: ```ts const agents = { builder: harnesses.codex({ model: "gpt-5.6-sol" }), reviewer: harnesses.claude({ model: "opus" }), }; export default defineWorkflow({ inputs, requires: { agents, models: [summarizer] }, workflow: ship, }); ``` jigs reads the harness kinds from the agents. The service checks those harness CLIs when it starts, and preflight checks everything listed before each run. A factory whose workflows run no agent needs no harness installed. jigs does not track spend. Watch it in each provider's own dashboard. ## Agent sessions A workflow often talks to the same agent several times: a builder that writes code, hears the review, and fixes it. `agentSession` is that agent across turns. Each `run` resumes the harness session it holds, so the agent remembers the turns before it. ```ts interface AgentSession { readonly harness: Harness; run(turn: { resume: Prompt; fresh: Prompt; output: z.ZodType }): Promise; run(turn: { resume: Prompt; fresh: Prompt }): Promise; } type Prompt = string | (() => Promise); function agentSession(options: { name: string; harness: Harness; cwd: string }): AgentSession; ``` A turn states the job twice. `resume` goes to an agent that already holds the earlier turns, so it carries only what is new. `fresh` goes to an agent starting from nothing, so it carries everything. Pass a function for either one when building it costs a step, such as reading a diff: only the prompt that is sent gets built. ```ts import { agentSession } from "#jigs/routines"; import { readWorktreeDiff } from "#jigs/steps"; const builderSession = agentSession({ name: "builder", harness: agents.builder, cwd }); for (let round = 1; round <= 3; round++) { const report = await builderSession.run({ output: implementationReport, resume: `The reviewer found:\n${findings}`, fresh: async () => `${task}\n\nThe work so far:\n${await readWorktreeDiff(cwd, baseSha)}\n\n${findings}`, }); // ... } ``` A harness session can be lost: a restart, a thread the harness no longer has, a harness you changed between deploys. When that happens `run` sends `fresh` to a new session and the workflow carries on, instead of failing in round four. Give an independent reviewer an agent session of its own. The data that makes a resume possible is a session reference, `AgentSessionRef`. `runAgent` returns it as `session` and takes it back as `resume`. You need it only when you call `runAgent` directly; an agent session keeps its own. It is plain data, which is why an agent session survives replay: the workflow rebuilds the object on every replay, and the reference comes back from the recorded steps. A session reference records the harness it was made on: its kind, and the whole descriptor. An agent session resumes a reference only on the same descriptor, compared by value. So a deploy that changes an agent's model or settings starts that agent fresh on its next turn, rather than resuming a session another configuration made. ## Harness settings A Claude Code or Codex descriptor is the provider's own settings, plus the model. jigs invents no setting names: whatever the provider accepts, and can be written down as data, you can set. ```ts type ClaudeHarness = JsonOnly> & { kind: "claude"; model: string; mcpServers?: Record; }; type CodexHarness = JsonOnly> & { kind: "codex"; model: string; mcpServers?: Record; }; ``` Each constructor takes one object: the model and any settings. Before this release the model was a separate first argument: ```ts // before harnesses.claude("opus", { effort: "high" }); harnesses.codex("gpt-5.6-sol"); // after harnesses.claude({ model: "opus", effort: "high" }); harnesses.codex({ model: "gpt-5.6-sol" }); ``` The constructors reject a key the descriptor does not have, even when the object comes from a variable, so pass only descriptor keys. ```ts harnesses.claude({ model: "opus", effort: "high", maxTurns: 40, allowedTools: ["Read", "Edit", "Bash"], maxBudgetUsd: 5, fallbackModel: "sonnet", }); harnesses.codex({ model: "gpt-5.6-sol", personality: "pragmatic", developerInstructions: "Prefer small commits.", }); ``` The provider's type is the list of knobs, so a new provider setting needs no jigs release. The cost is that a provider renaming a setting becomes a compile error when you upgrade, the same as any other contract jigs changes. Two kinds of setting are left out. A setting whose value is a function, such as a hook, a tool-approval callback or a logger, cannot be written down, and a descriptor has to be, because a workflow hands it to a step through the database. And a setting jigs sets itself or holds as policy is a compile error at the constructor. The drivers also drop those keys at run time and apply their policy last, so policy always wins. To hand the provider a function, [write your own agent step](/guide/custom-agent-step). `mcpServers` keeps jigs' shape on both harnesses: each server names a probe tool, which jigs calls before the agent starts. Some kept keys reach outside the worktree. `additionalDirectories` gives Claude Code more directories to work in. `debugFile` writes the SDK's debug log to a path of your choosing. Both are allowed because an agent step already runs with permissions bypassed and can reach the whole filesystem: the isolation boundary is the environment allowlist, not the filesystem. ## Claude Code `harnesses.claude({ model, ...settings })` works with `runAgent` and `askAgent`. It runs the `claude` CLI on the service's `PATH`, or the path in `JIGS_CLAUDE_EXECUTABLE`, logged in with `claude auth login`. It bills that account. `askAgent` sends only the model: it always runs without tools, MCP servers or filesystem settings. A Claude Code descriptor cannot name these keys (`ClaudePolicyKey`). `cwd` and `env`. Each step sets the worktree the agent works in and builds its environment from the allowlist, so a descriptor cannot point it elsewhere or hand it variables the allowlist would not. `pathToClaudeCodeExecutable`, `executable` and `executableArgs`. jigs runs the CLI the service checked at startup, with the launch hook that replaces the provider's environment. A descriptor that named another program would skip both. `permissionMode` and `allowDangerouslySkipPermissions`. An agent step runs unattended in its own worktree, so it always bypasses permission prompts. There is no one to answer a prompt. `strictMcpConfig`, `mcpServers` in the provider's shape, and `settingSources`. The agent sees exactly the MCP servers its descriptor lists, each with a probe jigs runs first, and loads only the project's settings. Global and user configuration never leak into a run. `agents`, `settings` and `plugins`. An inline subagent can declare MCP servers that strict mode allows and jigs never probes. An extra settings layer, inline or from a file path, can set environment variables, an API key helper, permissions and hooks behind the allowlist, and a path hides its contents from the session reference. A plugin loads hooks and agent definitions from any path on the host. Subagents, plugins and extra settings come from the repository through project settings, where the worktree owns them, not from the descriptor. `resume`, `continue`, `sessionId`, `forkSession`, `persistSession`, `resumeSessionAt` and `resumeDropsTurn`. The session belongs to the session reference jigs records and resumes. A descriptor that started, forked or dropped sessions on its own would make that reference lie. `extraArgs` and `sdkOptions`. Raw CLI arguments and SDK options can set any of the above, so allowing them would undo the whole list. ## Codex `harnesses.codex({ model, ...settings })` works with `runAgent` only. Codex has no mode without tools, so `askAgent` refuses it. It runs the `codex` CLI on the service's `PATH`, logged in with `codex login`. The service refuses to start when that CLI is older than the minimum version it names. A Codex descriptor cannot name these keys (`CodexPolicyKey`). `cwd`, `env` and `codexPath`. Each step sets the worktree, builds the environment from the allowlist, and launches the checked CLI through a launcher that passes only the allowed variables, from a private home. A descriptor that named another program or environment would skip all three. `approvalPolicy`, `sandboxPolicy` and `autoApprove`. An agent step runs unattended with full access to its worktree. It never waits for an approval nobody will give, and a narrower sandbox would make tool calls fail quietly. `threadMode`, `resume` and `persistExtendedHistory`. The thread belongs to the session reference jigs records and resumes, so a descriptor cannot change how threads are kept or which one runs. `mcpServers` in the provider's shape, and `configOverrides`. The agent sees exactly the MCP servers its descriptor lists, each probed first. Config overrides can rewrite the sandbox and MCP tables, so allowing them would undo both. ## Pi `harnesses.pi(source, options)` works with `runAgent` and `askAgent`. Its first argument is a model source, so one harness can run models from several providers: `models.openaiCodex(...)`, `models.openrouter(...)` or `models.openaiCompatible(...)`. * Install Pi 0.85.1 or newer and keep it on the service's `PATH`: `npm install --global @earendil-works/pi-coding-agent`. * For the OpenAI Codex subscription source, run `pi`, choose `/login`, then OpenAI Codex. * `options.thinking` sets the thinking level. For `runAgent`, `options.tools` limits Pi to a list such as `["read", "bash"]`; leave it out for Pi's default tools. * `options.mcpServers` is the complete set of MCP servers Pi sees. Pi never reads your global or project MCP files. Each server lists the tools the model may call, and names environment variables for its secrets, never the values. Each Pi call gets its own private home directory, so parallel calls never share settings or sessions. ## OpenRouter `models.openrouter(model)` works with `askModel`, `askJev`, and as a Pi source. Set `OPENROUTER_API_KEY` in the factory's `.env`, then run `jigs service restart`. To read a different variable, pass `models.openrouter(model, { apiKeyEnv: "TEAM_OPENROUTER_KEY" })`. Direct calls use strict structured output, so pick a model that supports [`structured_outputs`](https://openrouter.ai/models?supported_parameters=structured_outputs). ## OpenAI-compatible `models.openaiCompatible({ name, baseUrl, model, apiKeyEnv? })` works with `askModel` and as a Pi source. `baseUrl` is the server's full API base, including `/v1`, and `model` is an ID its `/models` endpoint returns. Most local servers need no key; for one that does, name its `.env` variable with `apiKeyEnv`. Before each run, preflight asks `/models` whether the model is served. ## Jev decisions `askJev` asks a jev decision model named questions about one piece of state and returns probabilities rather than prose: ```ts import { models, score, yesNo } from "@jigs-ai/jigs"; import { askJev } from "#jigs/routines"; const result = await askJev({ model: models.openrouter("typesafe/jev-1.13"), state: { crm: { name: "Acme Labs", domain: "acme.example" }, billing: { name: "Acme Labs LLC", domain: "acme.example" }, }, questions: { samePair: score("How closely do these records align?", [ "Different companies", "Possibly the same company", "The same company", ]), sameDomain: yesNo("Do the domains refer to the same entity?"), }, }); ``` `yesNo` returns a probability, `choice` a selected option with a probability for each, and `score` a probability per level plus a weighted score. How much probability is enough to act on is your workflow's decision. ## Agent environment Agents do not inherit the service's environment. Each starts with a small base set such as `PATH` and `HOME`, plus what its harness needs. To pass anything else, list its name under [`agents.env`](/guide/configuration#agents-env). --- --- url: https://salimhamed.github.io/jigs/guide/custom-agent-step.md --- # Write your own agent step Most workflows never need more than `runAgent` and a harness descriptor. When one does, it writes its own step, the way it writes any step, and asks jigs for the assembled runner instead of rebuilding the setup by hand. ## The runner `createAgentRunner` opens a Claude Code or Codex harness the way the built-in agent step does, and hands back the live provider model. ```ts import { createAgentRunner } from "@jigs-ai/jigs/steps"; interface AgentRunner { model: LanguageModel; sessionFrom(result: GenerateTextResult): AgentSessionRef | undefined; close(): Promise; } function createAgentRunner( harness: Harness, options: { cwd: string; run: RunMetadata; resume?: AgentSessionRef | undefined }, ): Promise; ``` Before it returns, it does everything the built-in step does before it calls the provider: it builds the agent's environment from the allowlist and your `agents.env`, runs the request and just-in-time checks, locks the worktree, gives Codex a private home and its own app server, and installs the Claude launch hook. `model` is ready to pass to the AI SDK. `close` releases the lock and stops what the harness started. ## A step that uses it Put the step in the workflow's own `steps.ts`. The step file needs the AI SDK, so add it to the factory: `pnpm add ai@7`. ```ts // workflows/my-flow/steps.ts import type { AgentSessionRef, Harness } from "@jigs-ai/jigs"; import { createAgentRunner } from "@jigs-ai/jigs/steps"; import { generateText } from "ai"; import { getWorkflowMetadata } from "workflow"; export async function runWithTemperature(request: { harness: Harness; cwd: string; prompt: string; resume?: AgentSessionRef | undefined; }) { "use step"; const runner = await createAgentRunner(request.harness, { cwd: request.cwd, run: getWorkflowMetadata(), resume: request.resume, }); try { const result = await generateText({ model: runner.model, prompt: request.prompt, temperature: 0 }); return { text: result.text, session: runner.sessionFrom(result) }; } finally { await runner.close(); } } ``` The workflow calls it like any step, with a descriptor from its `agents`: ```ts const answer = await runWithTemperature({ harness: agents.builder, cwd: worktree.path, prompt: "Explain the failing test.", }); ``` Always close the runner in a `finally`. Until it is closed, no other agent can start in that worktree. ## Where functions go A step is the one place a factory can hand the provider a function, such as a tool-approval hook or a logger. A descriptor is data the workflow writes to the database for the step to read, so it can hold no function. A step runs in a worker, where functions are allowed, so pass them through the AI SDK call your step makes. ## When it throws `createAgentRunner` throws a `JitCheckError` when a just-in-time check fails, such as an MCP server that does not answer its probe. It throws an `AgentSessionError`, exported beside it, when `resume` names a session this harness cannot resume: catch it and start again without `resume` if the step can. The built-in step turns both into what `runAgent` and `agentSession` expect; your step decides for itself. Pi has no AI SDK provider model: jigs runs the Pi CLI and reads its output directly. So `createAgentRunner` throws for a Pi descriptor. Run Pi with `runAgent`. ## Why a runner and not a plugin The alternative was to let a factory inject its own functions into jigs' step. A step of your own needs no new idea: it reads like every other step in the factory, and jigs' policy still applies, because the runner applies it. Adding a new harness kind from a factory is not supported yet. --- --- url: https://salimhamed.github.io/jigs/guide/recipes.md --- # Recipes A recipe is a complete workflow that ships with jigs as source code. Adding one copies its files into your factory, where they become your code: a starting point to read, run and change. Upgrading jigs never overwrites them. ## Add a recipe Inside your factory: ```sh pnpm exec jigs recipe list pnpm exec jigs recipe add linear-ticket-to-pr ``` `recipe add` reports each file it creates, and keeps any file that already exists. It registers the workflow by adding this line to the `workflows` map in `jigs.config.ts`: ```ts "linear-ticket-to-pr": () => import("./workflows/linear-ticket-to-pr/linear-ticket-to-pr.ts"), ``` Then run `pnpm exec jigs up`. ## Available recipes ### linear-ticket-to-pr Takes a Linear ticket to a merged pull request, with one agent building the change and a second reviewing it. After publication, the builder continues in the same agent session to handle GitHub feedback and failing checks. If the harness loses the session, a fresh prompt supplies the ticket, diff and PR facts. `recipe add` copies it to `workflows/linear-ticket-to-pr/`: the workflow file, a `delivery/` directory with its phases and prompts, its tests, and a README that says what it needs and how to change it. ```sh pnpm exec jigs run linear-ticket-to-pr --input ticket=AGE-123 --input binding=app ``` | Input | Default | What it chooses | | --- | --- | --- | | `ticket` | | The Linear ticket, by identifier or ID. | | `binding` | | The repository to change. | | `builder` | `builder` | The agent, by name, that builds the change. | | `reviewer` | `reviewer` | The agent, by name, that reviews it. | | `budget` | `{ reviewRounds: 3, attemptsPerUpdate: 3 }` | Implementation review rounds, and agent attempts allowed for each PR update. `attemptsPerUpdate` must be positive. | The agent names are `builder` and `reviewer`, defined in the workflow file. A run picks among them; it cannot name a model. To change a model, edit the agent in the workflow file. The recipe uses [`watchPullRequest`](/guide/build-a-workflow#wait-on-a-pull-request) to read current PR facts after changes. The builder decides whether to change code, answer feedback or do nothing, then returns `finished`, `pending` or `needs-human` with a summary. It can post through its own GitHub tools without jigs markers. Configure GitHub access for the builder's harness, such as an authenticated `gh` command or GitHub MCP. Agents do not automatically inherit the service's `GITHUB_TOKEN`, and configuring a GitHub App for jigs does not configure agent tools. For a tool using a token from the service environment, explicitly allow its variable through [`agents.env`](/guide/configuration#agents-env). No JEV model is required. Each changed snapshot starts a fresh `attemptsPerUpdate` allowance, including the first snapshot. There is no lifetime limit on PR updates. An agent can read an update and decide nothing needs doing; that is an ordinary successful visit. Duplicate notifications with unchanged facts do not invoke it again. If recovery already assessed an update, a later watcher delivery of those exact facts is also skipped; facts the agent has not seen still receive an assessment. After each attempt, recipe code checks the local work and the GitHub head. If the worktree is dirty, commits are unpublished, or the local branch is out of sync, it immediately gives the same agent the problem and another attempt. It does not wait for another GitHub notification to recover. Short, bounded reads allow GitHub's head to catch up after a push; these reads do not spend agent attempts. The allowance includes the initial attempt and resets only when the watcher yields a new update. Recovery cannot reset its own budget. If the agent requests human attention or exhausts its attempts without leaving the work ready to wait, the run fails with an explanation. It preserves local changes and does not automatically push them while stopping. The budget and recovery policy are in the copied recipe, so you can change them. The builder is instructed not to merge. This is a prompt rule, not a restriction on its GitHub tools. Recipe code follows the binding's [merge policy](/guide/configuration#merge): in human mode it waits for you; in automatic mode it requires the builder to report finished and still checks GitHub approval, CI and mergeability before merging. An agent's judgment does not replace those checks. If a merge attempt is refused, the recipe fails with the reason so you can inspect the PR before starting another run. ## Updating a recipe you already added `recipe add` never overwrites a file you have, and leaves an existing `workflows` entry in `jigs.config.ts` as it is. To take a newer version of linear-ticket-to-pr: 1. Finish or cancel the runs that use it. Moving or renaming a workflow file or function changes its durable ID. 2. Move your copy out of `workflows/`, or delete it. Anything left under `workflows/` is still typechecked and tested with the factory. An older copy may be the file `workflows/linear-ticket-to-pr.ts`, its `workflows/linear-ticket-to-pr.test.ts`, and the `workflows/linear-ticket-to-pr/` directory; move all three. ```sh mkdir -p ../old-linear-ticket-to-pr mv workflows/linear-ticket-to-pr* ../old-linear-ticket-to-pr/ ``` 3. Add the recipe again: `pnpm exec jigs recipe add linear-ticket-to-pr`. 4. Check that the `workflows` entry in `jigs.config.ts` imports `./workflows/linear-ticket-to-pr/linear-ticket-to-pr.ts`. 5. Configure GitHub tools for the builder harness so it can read discussions, post replies and push fixes. 6. Carry your own edits across, and launch with the new inputs above. Replace old `ciFixes` and `revisionRounds` budgets with `attemptsPerUpdate`. `jigs run` rejects an `--input` the workflow does not declare, so an old input such as `implementationModel` fails before the run starts. --- --- url: https://salimhamed.github.io/jigs/guide/configuration.md --- # Configuration A factory is configured in two files. `jigs.config.ts` holds settings you commit. `.env` holds secrets and is never committed. There is no other configuration file. After editing `jigs.config.ts`, run `jigs up`; it rebuilds and restarts the service when needed. After editing `.env`, run `jigs service restart`, because the service reads it when it starts. ```ts import { defineFactory } from "@jigs-ai/jigs"; export default defineFactory({ service: { port: 8990, dashboardPort: 9090 }, bindings: { app: { remote: "git@github.com:owner/app.git" }, }, github: { identities: [{ mode: "pat" }] }, linear: { identity: { mode: "key" } }, merge: { by: "human", method: "squash", approval: { kind: "label", name: "jigs:approved" } }, workflows: { hello: () => import("./workflows/hello/hello.ts"), }, }); ``` ## `service` | Key | Default | Meaning | | --- | --- | --- | | `port` | `8990` | Where the service listens. The CLI talks to it here. | | `dashboardPort` | required | Where the service hosts the run dashboard. | | `pollIntervalSeconds.github` | `300` | How often waiting runs re-read their pull requests. Minimum 30. | | `pollIntervalSeconds.linear` | `300` | How often runs waiting on a ticket reply re-read it. Minimum 30. | `jigs init` picks ports for each factory so that two factories on one machine rarely clash. The service and dashboard ports live here. The Postgres port lives in `docker-compose.yml` and in `WORKFLOW_POSTGRES_URL` in `.env`; change both together. ## `workflows` A map from a workflow's name to a deferred import of its file. The name is what `jigs run` takes. See [Build a workflow](/guide/build-a-workflow). ```ts workflows: { triage: () => import("./workflows/triage.ts"), }, ``` ## `bindings` {#bindings} A binding names a target repository. jigs keeps its own clone of each one, outside your checkout, and cuts every run's worktree from it. The service makes the clones when it starts, so run `jigs up` after adding a binding. ```ts bindings: { app: { remote: "git@github.com:owner/app.git", copy: [".env"], postCreate: ["pnpm install"], hookTimeoutMinutes: 20, merge: { by: "jigs" }, }, }, ``` | Key | Default | Meaning | | --- | --- | --- | | `remote` | required | The repository's Git remote URL. | | `copy` | `[]` | Files to copy into each new worktree. | | `postCreate` | `[]` | Commands to run in each new worktree, in order. The first failure stops provisioning. | | `hookTimeoutMinutes` | `10` | The total time `postCreate` may take. | | `merge.by`, `merge.method` | the factory's | Override the [merge policy](#merge) for this repository. | Each `copy` entry is a path, or a glob, inside `bindings//` in the factory, and lands at the same path in the worktree. `bindings/app/.env` arrives as `.env` at the worktree root. Keep secret files there; the scaffold's `.gitignore` already ignores every `.env`. An entry that matches nothing fails the worktree with a message naming it. `jigs bind ` adds a binding with its `remote`, and `jigs unbind ` removes one; add the other keys by hand. Both commands edit a plain object literal. If `bindings` is computed, they explain why and leave the file alone. ## `schedules` {#schedules} Fire a workflow on a cron schedule: ```ts schedules: { "monday-report": { workflow: "weekly-report", cron: "0 9 * * 1", inputs: { audience: "team" }, }, }, ``` `cron` has five fields, read in the service host's local time. Each tick is an ordinary run: its inputs are checked and preflight runs. A tick is skipped while the schedule's previous run is still active, and ticks missed while the service was down are not made up. `jigs status` lists schedules under the runs, and runs a schedule started show `schedule:` as their trigger. ## `release` {#release} What happens to a run's worktrees and scratch directory once it ends: ```ts release: { onSuccess: "release", onFailure: "keep" }, ``` That is the default. `onSuccess` applies to completed runs and `onFailure` to failed and cancelled ones. A workflow's `defineWorkflow` can set its own `release`, and a workflow can call `await release()` from `#jigs/routines` as its last step when it needs the report. Waiting runs always keep everything. Release never throws away work: a worktree with uncommitted or unmerged changes stays, and a branch is deleted only when its commits are proven merged. See `jigs resources` in [CLI commands](/guide/cli) to inspect what is left. ## `merge` {#merge} Who merges a pull request, how, and on what signal: ```ts merge: { by: "human", method: "squash", approval: { kind: "review" }, }, ``` * **`by`**: `"human"` means jigs follows the pull request and answers feedback, and you press Merge. `"jigs"` means jigs merges it once it is ready. Default `"human"`. * **`method`**: `"squash"`, `"merge"` or `"rebase"`, as on GitHub. Default `"squash"`. With `squash` and `merge`, the pull request title becomes the commit title. With `rebase`, each commit is rewritten and loses its signature. * **`approval`**: what counts as your consent. `{ kind: "review" }` is an approving review of the current commit; a new push withdraws it. `{ kind: "label", name: "jigs:approved" }` is a label on the pull request; it survives later pushes, so it means "merge whenever ready". GitHub does not let you approve your own pull request, so the label is the signal to use when jigs acts as you ([PAT mode](#github-identity)). `jigs init` writes the pairing that fits the identity you chose. A binding may override `by` and `method`, but not `approval`. `resolveMergePolicy(binding)` reads these settings for factory code. The linear-ticket-to-pr recipe checks `by` before calling `mergePullRequest`, which rereads GitHub and enforces readiness and approval. `watchPullRequest` only reports facts: it neither consumes a merge policy nor performs a merge. Custom workflows must apply `by` themselves. These settings do not restrict an agent that merges independently through its own GitHub tools. jigs merges only when the approval signal is present, GitHub reports the pull request mergeable, it is not a draft, and at least one check has run and passed. **jigs never merges in a repository with no CI**, so set `merge.by: "human"` for such a binding. While GitHub reports `behind`, `blocked` or `unknown`, jigs waits and checks again later. A label cannot satisfy a branch rule that requires approving reviews, so label approval only works on repositories without that rule. `jigs doctor` prints each binding's effective policy. When jigs merges, it also reports a repository with no CI, a disabled merge method, a missing label, or a required-review rule the label cannot meet. jigs never changes branch protection itself. ## `agents.env` {#agents-env} Agents do not inherit the service's environment. Each harness starts with a base set: `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TERM`, locale variables, `TZ`, `TMPDIR`, the XDG directories, proxy settings and CA certificate settings, plus the variables its own harness needs. Give agents anything else by name: ```ts agents: { env: ["SSH_AUTH_SOCK", "MISE_DATA_DIR"] }, ``` The list holds names only; the values come from the service's environment when an agent starts. Model keys such as `OPENROUTER_API_KEY` and variables jigs sets itself cannot be listed; name a model key on its model source instead. This limits what agents see in their environment only. They still run as your user and can read any file you can. ## GitHub identity {#github-identity} `github.identities` says who jigs is on GitHub. Choose the mode when you create the factory, with `jigs init --github-identity-mode pat` (the default) or `app`. ### PAT: jigs acts as you ```ts github: { identities: [{ mode: "pat" }] }, ``` Put a personal access token in `.env` as `GITHUB_TOKEN`. Pull requests jigs opens are authored by you, so GitHub will not let you approve them: use label approval. You can still send work back with review comments or a comment on the pull request. A classic token needs `repo` (or `public_repo`), plus `admin:repo_hook` if you turn on GitHub webhooks. ### App: jigs acts as a bot ```ts github: { identities: [{ mode: "app", appId: 123456, installations: { owner: 7654321 }, privateKeyPath: "github-app.private-key.pem", operator: "your-github-login", coAuthor: "Your Name ", }], }, ``` Pull requests come from `[bot]`, and you review them like anyone else's. `jigs init --github-identity-mode app` takes all of these values as flags. To set one up: 1. **Register a GitHub App** under Settings → Developer settings → GitHub Apps. Leave OAuth and device flow off, and turn its webhook off. 2. **Grant repository permissions**: Contents, Pull requests and Issues read and write; Administration read; Metadata, Checks and Commit statuses read. Add Actions read when jigs merges, and Repository webhooks read and write if you turn on GitHub webhooks. `jigs doctor` names any that are missing. 3. **`appId`** is the App ID on its settings page. 4. **`privateKeyPath`** is the key GitHub generates under Private keys. Save it in the factory (`.gitignore` already excludes `*.private-key.pem`) and run `chmod 600` on it; `jigs doctor` fails on a looser mode. 5. **`installations`**: install the App on the repositories you bind. The installation's URL ends in its ID; add it under the account name. 6. **`operator`** is your GitHub login. jigs assigns pull requests to you. **`coAuthor`** is optional and adds a `Co-authored-by` line to merge commits. Every GitHub binding needs an installation for its owner. To use different Apps for different organizations, add more entries to `identities`; no two may claim the same account. A PAT must be the only entry. ## Linear identity {#linear-identity} `linear.identity` says who jigs is on Linear. Choose it with `jigs init --linear-identity-mode key` (the default) or `app`. * **`key`: jigs acts as you.** Put a Linear personal API key in `.env` as `LINEAR_API_KEY`. Linear does not notify you of your own comments, so when a run asks you a question on a ticket, the mention may never reach your inbox. A key for a separate Linear user avoids this. * **`app`: jigs acts as an app.** Its comments and mentions reach you like anyone else's. In Linear, go to Settings → API → OAuth applications and create one with **Client credentials** on, Public off and Webhooks off (any redirect URL will do). Put its ID and secret in `.env` as `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET`, then run `jigs service restart`. ```ts linear: { identity: { mode: "app" } }, ``` ## Webhooks {#webhooks} Webhooks are optional. Without them, waiting runs re-read GitHub and Linear every [`pollIntervalSeconds`](#service), and nothing else is needed. Webhooks make runs react in seconds. The poll keeps running underneath, so a lost delivery only delays a run. ```ts webhooks: { url: "https://my-machine.my-tailnet.ts.net", github: { enabled: true }, linear: { enabled: false }, }, ``` 1. **Expose the service port** with a tunnel, for example `tailscale funnel --bg ` or `cloudflared tunnel --url http://localhost:`. The public URL is `webhooks.url`. 2. **GitHub**: create a secret with `openssl rand -hex 32`, put it in `.env` as `GITHUB_WEBHOOK_SECRET`, run `jigs service restart`, then run `jigs bind` again for each repository. `bind` creates or repairs the repository's webhook. It needs hook permissions: `admin:repo_hook` for a PAT, or Repository webhooks read and write for an App. 3. **Linear**: create the webhook yourself in Linear under Settings → API → Webhooks, pointing at `/ingress/linear`, for `Comment` events only. Put its signing secret in `.env` as `LINEAR_WEBHOOK_SECRET` and run `jigs service restart`. A provider that is enabled without its secret stops the service from starting. `jigs doctor` checks the secrets and, for GitHub, whether recent deliveries were rejected. ## The `.env` file `jigs init` writes `.env.example`. Copy it to `.env`; `jigs up` stops if `.env` is missing, and lists the credentials still empty. | Variable | When you need it | | --- | --- | | `WORKFLOW_TARGET_WORLD`, `WORKFLOW_POSTGRES_URL` | Always. Filled in by `jigs init`; leave them. | | `GITHUB_TOKEN` | GitHub [PAT mode](#github-identity), once you bind a GitHub repository or a workflow requires `github`. | | `LINEAR_API_KEY` | Linear [`key` mode](#linear-identity). | | `LINEAR_CLIENT_ID`, `LINEAR_CLIENT_SECRET` | Linear [`app` mode](#linear-identity). | | `GITHUB_WEBHOOK_SECRET` | GitHub [webhooks](#webhooks) enabled. | | `LINEAR_WEBHOOK_SECRET` | Linear [webhooks](#webhooks) enabled. | | `OPENROUTER_API_KEY` | Workflows that use `models.openrouter()`. | | `JIGS_CLAUDE_EXECUTABLE` | Optional. Path to `claude` when it is not on the service's `PATH`. | | `AWS_PROFILE` | Workflows that declare `requires: { aws: true }`. Preflight checks the profile with `aws sts get-caller-identity`. | `JIGS_SERVICE_URL` is read by the CLI, not the service. Set it in your shell to point commands such as `jigs status` at a different service, or pass `--service-url`. --- --- url: https://salimhamed.github.io/jigs/guide/cli.md --- # CLI commands Run every command inside your factory, as `pnpm exec jigs `, so it uses that factory's installed version of jigs. The exception is `init`, which runs before there is a factory: `pnpm --config.minimum-release-age-exclude=@jigs-ai/jigs dlx @jigs-ai/jigs init`. Add `--help` to a command to see its options. ## Everyday commands | Command | What it does | | --- | --- | | `jigs init` | Create a factory in the current directory. Keeps existing files. | | `jigs up` | Install, start Postgres, build, start the service, wait until ready, then run `jigs doctor`. | | `jigs down` | Stop the service, then Postgres (`docker compose down`). Postgres's data is kept. | | `jigs workflows` | List the workflows the running service can run, and their inputs. | | `jigs run --input key=value` | Start a run. Repeat `--input` for each input. | | `jigs status [run]` | Show all runs and schedules, or one run's steps, result, resources and what it waits for. | | `jigs watch [run]` | Follow all runs, or one, printing a line per change. | | `jigs cancel ` | Cancel a run. | | `jigs doctor` | Check configuration, credentials and tools against the running service. | | `jigs upgrade` | Move the factory to the latest jigs and bring it up. | ## Repositories | Command | What it does | | --- | --- | | `jigs bind ` | Add a binding for a repository, and create its approval label or webhook when configured. | | `jigs bindings` | List bindings, their clone paths and whether each clone exists. | | `jigs unbind ` | Remove a binding. The clone stays on disk for you to delete. | ## Recipes | Command | What it does | | --- | --- | | `jigs recipe list` | List the recipes that ship with jigs. | | `jigs recipe add ` | Copy a recipe into the factory, keeping existing files. See [Recipes](/guide/recipes). | ## Resources | Command | What it does | | --- | --- | | `jigs resources list` | List each run's worktrees, scratch directories and recorded resources. Changes nothing. | | `jigs resources prune` | Preview what could be safely removed. | | `jigs resources prune --apply` | Remove it. Needs the service stopped. | ## Service | Command | What it does | | --- | --- | | `jigs service start` | Start the service from the current build and wait until it is ready. | | `jigs service stop` | Stop the service, giving in-flight work a few seconds to finish. Postgres keeps running. | | `jigs service restart` | Stop, then start. | | `jigs service status` | Say whether the service runs, with its service and dashboard URLs. | | `jigs service logs` | Print the service's recent output. `--lines` sets how many. | The service hosts its own dashboard. Do not run the Workflow SDK's `workflow web` against a factory; see [Troubleshooting](/guide/troubleshooting#runs-stop-moving-after-you-ran-workflow-web). ## Advanced | Command | What it does | | --- | --- | | `jigs build` | Compile the workflows into the service bundle. `jigs up` runs it for you. | | `jigs generate` | Refresh the generated `jigs/steps.ts` and `jigs/routines.ts` from the installed jigs version. | | `jigs poke ` | Make a waiting run check its condition now. It does not answer the wait for it. | ## Choosing a run Wherever a command takes a run, you can give a complete run ID, a unique prefix of one, or the ticket the run claimed, such as `AGE-123`. An ambiguous prefix lists the matches instead of guessing. `status`, `watch` and `resources` take `--json` for machine-readable output. `--input` values are read as JSON when they parse, and as plain strings otherwise, so `count=3` is a number and `ticket=AGE-123` is a string. A value the workflow's schema rejects fails before any run is created. ## `jigs up` on a running service `jigs up` is also the command to run after every change. Each step is skipped when there is nothing to do, so an unchanged factory installs, migrates and restarts nothing. When the service is already running, `up` restarts it only if the built bundle or `jigs.config.ts` changed. `--restart-service` forces a restart. If any run has not finished, `up` lists those runs and asks before restarting over them; `--force` skips the question, and without a terminal to ask in, it refuses. If a step fails, `up` prints `FAIL ` with a repair on the next line. Fix it and run `jigs up` again. ## Upgrading jigs `jigs upgrade` moves the factory's jigs pin to the latest release, or to `--to-version `. Then, using the newly installed version, it regenerates `jigs/`, runs `jigs up` and runs the factory's typecheck. Review and commit the changes it makes. Your workflows and copied recipes are yours to update: a new release can change an API they use, and the typecheck tells you where. ## Cancelling `jigs cancel` ends a waiting run immediately, and asks first when the run is in the middle of a step (`--force` skips the question). A step already running may still finish its outside work, but the run does not continue. Cancel lists any worktrees it leaves behind. ## Cleaning up resources `jigs resources prune` only previews unless you add `--apply`. To apply: 1. Run `jigs service stop`. 2. Check the preview, optionally for one run with `--run `. 3. Run `jigs resources prune --apply`. Only resources of finished runs owned by this factory are removed. A worktree with uncommitted changes, an unmerged branch, a waiting run's resources and anything jigs cannot prove it owns are always kept. Resources a release policy chose to keep need `--include-kept`, which relaxes nothing else. Applying needs proof that the service and its agents have stopped, which jigs gets from the factory's systemd user scope, so it only works on Linux hosts with systemd. --- --- url: https://salimhamed.github.io/jigs/guide/troubleshooting.md --- # Troubleshooting jigs usually prints what failed and how to fix it on the next line. Start there. For service problems, these two commands give the most useful evidence: ```sh pnpm exec jigs service status pnpm exec jigs service logs ``` ## The service exits before it is ready Read `jigs service logs`. The usual causes: * **Docker is not running**, so Postgres is not up. * **A harness CLI is missing from the service's `PATH`.** The service checks the CLI of every harness your workflows require, and the log names the workflows that need it. Start `jigs up` from a shell where that CLI runs, or for Claude Code set `JIGS_CLAUDE_EXECUTABLE` in `.env`. * **Codex or Pi is too old.** The log names the minimum version; upgrade the CLI. * **A binding's remote cannot be reached.** The service clones every binding before it is ready, and exits with the Git error if it cannot. Fix the cause and run `jigs up` again. ## A build says `jigs/` is out of date Run `pnpm exec jigs generate`, review the change to `jigs/steps.ts` and `jigs/routines.ts`, then run `pnpm exec jigs up`. Keep your own code out of `jigs/`, since generating replaces it. A build also refuses a factory that still has a `jigs.ts` from an earlier release. Run `pnpm exec jigs upgrade`. It deletes `jigs.ts`, writes `jigs/`, and replaces the older entries in the `imports` map in `package.json` with `#jigs/*`. Then change your workflows to import from `#jigs/steps` and `#jigs/routines` instead of `#jigs`. ## A library import does not resolve `@jigs-ai/jigs` has one entry for workflow code: the root. Import descriptors, types, schemas and renderers from `@jigs-ai/jigs`. Import routines such as `claimTicket`, `agentSession`, `watchPullRequest` or `pullRequestGate` from `#jigs/routines`, after `pnpm exec jigs generate`. ## A run is waiting Run `pnpm exec jigs status `. A waiting run is expected when it asked a question or is following a pull request; the status says what it needs and links to where you act. Starting another run does not answer the first one. Once you have answered, the run notices on its next [check](/guide/configuration#webhooks). `pnpm exec jigs poke ` makes it check now. A poke cannot stand in for the answer or approval itself. If `jigs status` reports a run as `stalled`, nothing is going to move it; its detail view shows the step or queue job that died and how to requeue it. ## Doctor reports webhook deliveries rejected with 401 GitHub's copy of the webhook secret does not match `GITHUB_WEBHOOK_SECRET` in `.env`. Run `pnpm exec jigs bind ` for that repository to send GitHub the current secret. ## An old worktree or directory is still there That is often on purpose: failed runs, waiting runs and unfinished Git work keep their resources. Inspect them with `pnpm exec jigs resources list`, then preview `pnpm exec jigs resources prune` before you remove anything. See [CLI commands](/guide/cli#cleaning-up-resources). ## Runs stop moving after you ran `workflow web` Never run the Workflow SDK's standalone `workflow web` against a factory's database. It starts a queue worker that takes the factory's jobs. Stop it, and use the dashboard the service hosts instead. --- --- url: https://salimhamed.github.io/jigs/api.md --- # API reference The API has two halves. The root, `jigs`, is what workflow code imports: descriptors, types, schemas and pure renderers. `steps/*` do the real work, and your factory wires them in through its generated `jigs/steps.ts`. In workflow code, call routines such as `runAgent` from `#jigs/routines`, and steps such as `provisionWorktree` from `#jigs/steps`. ## Modules * [jigs](jigs.md) * [steps](steps.md) * [steps/agents](steps/agents.md) * [steps/git](steps/git.md) * [steps/human](steps/human.md) * [steps/linear](steps/linear.md) * [steps/pull-requests](steps/pull-requests.md) * [steps/runtime](steps/runtime.md) * [steps/workspaces](steps/workspaces.md) --- --- url: https://salimhamed.github.io/jigs/api/jigs.md --- # jigs Everything a factory's configuration and workflows import from jigs: the factory and workflow definitions, harness and model descriptors, the data steps hand back, question helpers, and pure renderers. Steps and routines come from your factory's generated `#jigs/steps` and `#jigs/routines`. ## Classes ### ClaimConflictError A ticket-claim failure that identifies the run already holding the ticket. #### Extends * `Error` #### Constructors ##### Constructor ```ts new ClaimConflictError(resource, owningRunId): ClaimConflictError; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `resource` | `string` | | `owningRunId` | `string` | ###### Returns [`ClaimConflictError`](#claimconflicterror) ###### Overrides ```ts Error.constructor ``` #### Properties | Property | Type | | :------ | :------ | | `owningRunId` | `string` | | `resource` | `string` | *** ### JigsError An operator-readable failure that is safe to construct inside a workflow. #### Extends * `Error` #### Constructors ##### Constructor ```ts new JigsError(message, hint?): JigsError; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `message` | `string` | | `hint?` | `string` | ###### Returns [`JigsError`](#jigserror) ###### Overrides ```ts Error.constructor ``` #### Properties | Property | Type | | :------ | :------ | | `hint?` | `string` | ## Interfaces ### AgentsDefinition Settings for the agent harnesses this factory runs. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `env?` | `string`\[] | Names of service environment variables every agent harness also receives. A harness otherwise starts with only a small base set, such as `PATH` and `HOME`, and the variables its own driver needs. Model credentials and the variables jigs sets itself are refused: name a model credential on its model source instead. | *** ### ChangePatch Patches for selected paths between two resolved commits. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `patches` | `object`\[] | Patch text for each selected path, in first-requested order. | | `truncated` | `boolean` | Whether the shared text limit cut off any patch text. | *** ### ChangeSummary A bounded description of the committed changes between two Git refs. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `base` | `string` | The resolved base commit. | | `commits` | `object`\[] | Commits reachable from head but not base, newest first. | | `files` | [`FileChange`](#filechange)\[] | Files that differ directly between the base and head trees. | | `head` | `string` | The resolved head commit. | | `truncated` | `boolean` | Whether file or commit limits caused results to be omitted. | *** ### CheckRun A check or commit status reported on a pull request head. #### Properties | Property | Type | | :------ | :------ | | `conclusion` | `string` | `null` | | `name` | `string` | | `url` | `string` | *** ### Factory What a factory repo hands the service: its workflows, keyed by name, and the schedules that fire them. A schedule is keyed by its own name rather than nested under a workflow — the name is what runs, status and `jigs doctor` refer to, and one workflow can carry several. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `schedules?` | `Record`<`string`, [`Schedule`](#schedule)> | - | | `webhooks?` | `object` | Which provider webhook routes the service mounts. Absent, it mounts none. | | `webhooks.github` | `object` | - | | `webhooks.github.enabled` | `boolean` | - | | `webhooks.linear` | `object` | - | | `webhooks.linear.enabled` | `boolean` | - | | `webhooks.url` | `string` | - | | `workflows` | `Record`<`string`, `AnyWorkflowDefinition`> | - | *** ### FactoryDefinition Operating settings and deferred workflow modules declared by a factory. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `agents?` | [`AgentsDefinition`](#agentsdefinition) | - | | `bindings?` | `Record`<`string`, { `copy?`: `string`\[]; `hookTimeoutMinutes?`: `number`; `merge?`: { `by?`: `"jigs"` | `"human"`; `method?`: `"squash"` | `"merge"` | `"rebase"`; }; `postCreate?`: `string`\[]; `remote`: `string`; }> | - | | `github?` | `object` | - | | `github.identities?` | ( | { `mode`: `"pat"`; } | { `appId`: `number`; `coAuthor?`: `string`; `installations`: `Record`<`string`, `number`>; `mode`: `"app"`; `operator`: `string`; `privateKeyPath`: `string`; })\[] | - | | `linear?` | `object` | - | | `linear.identity?` | | { `mode`: `"key"`; } | { `mode`: `"app"`; } | - | | `merge?` | `object` | - | | `merge.approval?` | | { `kind`: `"review"`; } | { `kind`: `"label"`; `name`: `string`; } | The signal that authorizes an automatic merge. | | `merge.by?` | `"jigs"` | `"human"` | Whether jigs merges an eligible pull request or waits for a person to merge it. | | `merge.method?` | `"squash"` | `"merge"` | `"rebase"` | The GitHub merge method to use when jigs performs the merge. | | `release?` | `object` | - | | `release.onFailure` | `"release"` | `"keep"` | What to do with eligible resources after a failed or cancelled run. | | `release.onSuccess` | `"release"` | `"keep"` | What to do with eligible resources after a completed run. | | `schedules?` | `Record`<`string`, [`Schedule`](#schedule)> | - | | `service` | `object` | - | | `service.dashboardPort` | `number` | - | | `service.pollIntervalSeconds?` | `object` | Seconds between the service's re-reads of each parked run, per provider. Each defaults to 300 and may not go below 30. Up to a tenth of the interval is taken off at random so services do not all poll at once. | | `service.pollIntervalSeconds.github?` | `number` | - | | `service.pollIntervalSeconds.linear?` | `number` | - | | `service.port?` | `number` | - | | `webhooks?` | `object` | - | | `webhooks.github` | `object` | - | | `webhooks.github.enabled` | `boolean` | - | | `webhooks.linear` | `object` | - | | `webhooks.linear.enabled` | `boolean` | - | | `webhooks.url` | `string` | - | | `workflows` | `Record`<`string`, () => `Promise`<{ `default`: `AnyWorkflowDefinition`; }>> | - | *** ### FileChange One file changed between the base and head trees. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `additions` | `number` | The number of added lines, or zero for a binary file. | | `deletions` | `number` | The number of deleted lines, or zero for a binary file. | | `path` | `string` | The changed path. Renames use the path in the head tree. | | `status` | [`ChangeStatus`](#changestatus) | How the path differs between the two trees. | *** ### HumanReply The first human ticket reply that wakes a halted run. #### Properties | Property | Type | | :------ | :------ | | `author` | `object` | | `author.id` | `string` | | `author.name` | `string` | | `body` | `string` | | `commentId` | `string` | | `createdAt` | `string` | *** ### PullRequestComment A comment on the pull request conversation, which hangs off no thread. #### Properties | Property | Type | | :------ | :------ | | `body` | `string` | | `createdAt` | `string` | | `id` | `number` | | `updatedAt` | `string` | | `user` | `string` | | `userType` | `string` | *** ### PullRequestMarker Hidden progress metadata stored in a pull request comment. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `kind` | `MarkerKind` | `reply` answers the thing named by `source`, `completion` records work finished for it, and `status` is a note about a commit — a stand-down after a refused merge, a CI failure jigs could not repair, or a merge refused for a state that will pass. | | `reason?` | [`StatusReason`](#statusreason) | Required on a `status` marker, meaningless on any other. | | `run` | `string` | The run that wrote it. Provenance for a reader; never matched on. | | `scope` | `string` | The continuation identity. It survives run replacement, so a later run answering for the same scope sees this work as its own and does not redo it. Another scope's marker means "some jigs workflow wrote this", never "my work is done". | | `source?` | `string` | What this answers: a comment as `id@updatedAt`, or a commit sha. | *** ### PullRequestReview A submitted GitHub review of a pull request. #### Properties | Property | Type | | :------ | :------ | | `body` | `string` | | `commitSha?` | `string` | | `id` | `number` | | `state` | `string` | | `submittedAt` | `string` | | `user` | `string` | *** ### PullRequestSnapshot GitHub facts about a pull request, without a judgment about outstanding work. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `ci` | `"red"` | `"green"` | `"pending"` | - | | `conversationComments` | [`PullRequestComment`](#pullrequestcomment)\[] | - | | `draft` | `boolean` | - | | `failingChecks` | [`CheckRun`](#checkrun)\[] | - | | `headSha` | `string` | - | | `labels` | `string`\[] | Label names on the pull request; the `label` approval signal reads these. | | `mergeCommitSha` | `string` | `null` | The merge commit, once GitHub has made one. | | `merged` | `boolean` | - | | `mergeState` | `string` | GitHub's own verdict on whether the pull request can merge right now, folding in conflicts, required checks and required reviews. `"clean"` is the only value that permits a merge; `"unknown"` means GitHub has not finished computing it, so the answer is "not yet, ask again". | | `reviews` | [`PullRequestReview`](#pullrequestreview)\[] | - | | `reviewThreads` | [`ReviewThread`](#reviewthread)\[] | - | | `state` | `"open"` | `"closed"` | - | *** ### ReleaseReport The result of applying a release policy to one run's managed resources. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `policy` | `object` | The policy applied by this release attempt. | | `policy.onFailure` | `"release"` | `"keep"` | What to do with eligible resources after a failed or cancelled run. | | `policy.onSuccess` | `"release"` | `"keep"` | What to do with eligible resources after a completed run. | | `runDirectory` | `ReleasedResource` | The scratch directory's local path, removal flag and reason for the result. | | `worktrees` | `ReleasedResource` & `object`\[] | Results for the run's worktrees, including paths, branches, removal flags, unmerged commit counts and reasons for anything retained. | *** ### ReviewComment A comment anchored to a file in a pull request review. #### Properties | Property | Type | | :------ | :------ | | `body` | `string` | | `createdAt` | `string` | | `id` | `number` | | `line` | `number` | `null` | | `path` | `string` | | `rootId` | `number` | | `updatedAt` | `string` | | `user` | `string` | *** ### ReviewThread A pull request review conversation, with its optional file location. #### Properties | Property | Type | | :------ | :------ | | `comments` | [`ReviewComment`](#reviewcomment)\[] | | `line` | `number` | `null` | | `origin?` | `"conversation"` | | `path` | `string` | | `rootId` | `number` | *** ### RunResource A durable thing that a run created or otherwise owns a reference to. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `identity` | `string` | The stable name that distinguishes this resource from others of the same kind. | | `kind` | `string` | The resource category, such as `worktree` or `run-directory`. | | `url` | `string` | An absolute URL where a human can inspect the resource. | *** ### Schedule One recurring trigger: a workflow, when to fire it, and the inputs to fire it with. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `cron` | `string` | Five fields, evaluated in the service host's local time zone. | | `inputs` | `Record`<`string`, `unknown`> | - | | `workflow` | `string` | - | *** ### ThreadAnswers Answers routed back to pull-request threads and an optional commit explanation. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `answers` | `object`\[] | Replies to post, using `null` to answer feedback on the pull request conversation. | | `commitExplanation` | `string` | `null` | A note explaining the pushed commit, or `null` when no explanation should be posted. | *** ### TicketClaim A ticket held exclusively by the current workflow run. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `hook` | `Hook`<`unknown`> | - | | `identifier` | `string` | - | | `issueId` | `string` | - | | `postedCommentIds` | `string`\[] | Every comment this run has posted on the ticket. A parked run skips these when it looks for a human's reply. | | `token` | `string` | - | *** ### WorkflowDefinition A workflow: its function, its input schema, and what a run needs before it may start. #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `S` *extends* `z.ZodType` | `z.ZodType` | #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `inputs` | `S` | - | | `release?` | `object` | - | | `release.onFailure` | `"release"` | `"keep"` | What to do with eligible resources after a failed or cancelled run. | | `release.onSuccess` | `"release"` | `"keep"` | What to do with eligible resources after a completed run. | | `requires?` | `WorkflowRequires` | What the workflow needs before a run can start: the agents it runs, the integrations, bindings and API model sources it uses. The service checks the CLI of every agent's harness when it starts, and preflight checks everything listed before every run. List only what the workflow uses. **Example** `const agents = { builder: harnesses.claude({ model: "opus" }), reviewer: harnesses.codex({ model: "gpt-5.6-sol" }), }; export default defineWorkflow({ inputs, requires: { agents, integrations: ["linear", "github"] }, workflow: shipTicket, });` | | `workflow` | (`inputs`) => `Promise`<`unknown`> | - | *** ### Worktree A provisioned repository worktree and the commit it was cut from. #### Properties | Property | Type | | :------ | :------ | | `baseSha` | `string` | | `branch` | `string` | | `defaultBranch` | `string` | | `path` | `string` | ## Type Aliases ### AgentResult ```ts type AgentResult = ModelResult & object; ``` A model result with the session reference an agent harness returned, when it returned one. #### Type Declaration | Name | Type | | :------ | :------ | | `session?` | [`AgentSessionRef`](#agentsessionref) | #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `T` | `unknown` | *** ### AgentSessionRef ```ts type AgentSessionRef = object; ``` A session reference: the small piece of data that lets a later `runAgent` call resume the same harness session. Pass it back as `resume`. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `descriptor` | `string` | The harness descriptor the session was recorded on, as [describeHarness](#describeharness) renders it. | | `harness` | [`Harness`](#harness-2)\[`"kind"`] | - | | `id` | `string` | - | *** ### ApprovalSignal ```ts type ApprovalSignal = z.output; ``` The review or label signal that authorizes an automatic merge. *** ### AskableHarness ```ts type AskableHarness = | ClaudeHarness & ToolFree | PiHarness & ToolFree; ``` A harness `askAgent` can run with no tools: Claude Code or Pi, without MCP servers or a Pi tool allowlist. Codex has no mode without tools. *** ### AskableModelSource ```ts type AskableModelSource = Exclude; ``` A model source accepted by a direct model call. *** ### AskAgentOptions ```ts type AskAgentOptions = object; ``` Options for one harness turn without tools or a worktree. #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `T` | `undefined` | #### Properties | Property | Type | | :------ | :------ | | `harness` | [`AskableHarness`](#askableharness) | | `output?` | `z.ZodType`<`T`> | | `prompt` | `string` | | `system?` | `string` | *** ### AskJevOptions ```ts type AskJevOptions = object; ``` A decision request in workflow and durable wire form. #### Type Parameters | Type Parameter | | :------ | | `QUESTIONS` *extends* [`JevQuestions`](#jevquestions) | #### Properties | Property | Type | | :------ | :------ | | `model` | [`OpenrouterSource`](#openroutersource) | | `questions` | `QUESTIONS` | | `state` | [`JevState`](#jevstate) | *** ### AskModelOptions ```ts type AskModelOptions = object; ``` Options for one API model call. #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `T` | `undefined` | #### Properties | Property | Type | | :------ | :------ | | `model` | [`AskableModelSource`](#askablemodelsource) | | `output?` | `z.ZodType`<`T`> | | `prompt` | `string` | | `system?` | `string` | *** ### BindingDefinition ```ts type BindingDefinition = z.input; ``` A repository this factory works in: its remote, how a worktree cut from it is provisioned, and any merge settings that differ from the factory's. #### Example ```ts bindings: { api: { remote: "git@github.com:acme/api.git", postCreate: ["pnpm install"], merge: { by: "jigs", method: "rebase" }, }, }, ``` *** ### ChangeStatus ```ts type ChangeStatus = "added" | "modified" | "deleted" | "renamed" | "other"; ``` How a file differs between the base and head trees. *** ### ChoiceQuestion ```ts type ChoiceQuestion = object; ``` A question answered with one named option. #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `OPTIONS` *extends* `Record`<`string`, `string`> | `Record`<`string`, `string`> | #### Properties | Property | Type | | :------ | :------ | | `instructions` | `string` | | `options` | `OPTIONS` | | `type` | `"choice"` | *** ### ClaudeHarness ```ts type ClaudeHarness = JsonOnly> & object; ``` A Claude Code harness descriptor: the provider's own settings that are data, minus each [ClaudePolicyKey](#claudepolicykey), plus the model and jigs' MCP server shape. #### Type Declaration | Name | Type | | :------ | :------ | | `kind` | `"claude"` | | `mcpServers?` | `Record`<`string`, [`McpServerConfig`](#mcpserverconfig)> | | `model` | `string` | *** ### ClaudeHarnessSettings ```ts type ClaudeHarnessSettings = Omit; ``` The one argument `harnesses.claude` takes: the model and any Claude Code settings. *** ### ClaudePolicyKey ```ts type ClaudePolicyKey = typeof claudePolicyKeys[number]; ``` A Claude Code setting a descriptor cannot name, because jigs sets it itself or holds it as policy. #### Remarks jigs sets the working directory, environment, executable and session for every step, and holds permissions, setting sources and MCP servers as policy. `extraArgs` and `sdkOptions` would rewrite any of those. `agents`, `settings` and `plugins` would bring in unprobed MCP servers, environment, permissions and hooks from outside the worktree; they come from the repository's project settings instead. *** ### CodexHarness ```ts type CodexHarness = JsonOnly> & object; ``` A Codex harness descriptor: the provider's own settings that are data, minus each [CodexPolicyKey](#codexpolicykey), plus the model and jigs' MCP server shape. #### Type Declaration | Name | Type | | :------ | :------ | | `kind` | `"codex"` | | `mcpServers?` | `Record`<`string`, [`McpServerConfig`](#mcpserverconfig)> | | `model` | `string` | *** ### CodexHarnessSettings ```ts type CodexHarnessSettings = Omit; ``` The one argument `harnesses.codex` takes: the model and any Codex settings. *** ### CodexPolicyKey ```ts type CodexPolicyKey = typeof codexPolicyKeys[number]; ``` A Codex setting a descriptor cannot name, because jigs sets it itself or holds it as policy. #### Remarks jigs sets the working directory, environment, executable, thread and session for every step, and holds the approval and sandbox policies and MCP servers. `configOverrides` would rewrite the sandbox and MCP tables. *** ### GitHubDefinition ```ts type GitHubDefinition = z.input; ``` Who jigs is on GitHub: the operator's own token, or a GitHub App installation. *** ### Halt ```ts type Halt = object; ``` What the ticket comment says, in the words a stranger to the repo reads. `headline` is one plain sentence naming what jigs paused and why, `where` names the routine it paused in so the footer can say so, `about` restates the ticket itself, `notes` are plain bullet lines, and `onReply` decides what the comment asks the human to do: choose between the questions ("continue") or repair something and let the step run again ("retry"). #### Properties | Property | Type | | :------ | :------ | | `about?` | `string` | | `headline` | `string` | | `notes?` | `string`\[] | | `onReply` | `"continue"` | `"retry"` | | `questions?` | [`HaltQuestion`](#haltquestion)\[] | | `where` | `string` | *** ### HaltOption ```ts type HaltOption = z.infer; ``` One answer choice for a question shown to a human. *** ### HaltQuestion ```ts type HaltQuestion = z.infer; ``` A question shown to a human while a run waits for their reply. *** ### Harness ```ts type Harness = | ClaudeHarness | CodexHarness | PiHarness; ``` A serializable agent-program descriptor. *** ### HarnessForOptions ```ts type HarnessForOptions = [Extract] extends [never] ? H & ToolFree : H; ``` The descriptor a harness constructor returns for its options. It is also [ToolFree](#toolfree), so `askAgent` accepts it, when the options name no tools or MCP servers. #### Type Parameters | Type Parameter | | :------ | | `H` | | `O` | *** ### HarnessKind ```ts type HarnessKind = Harness["kind"]; ``` The stable name of an agent harness. *** ### JevAnswer ```ts type JevAnswer = QUESTION extends ChoiceQuestion ? object : QUESTION extends ScoreQuestion ? object : object; ``` The calibrated answer shape selected by one question descriptor. #### Type Parameters | Type Parameter | | :------ | | `QUESTION` *extends* [`JevQuestion`](#jevquestion) | *** ### JevAnswers ```ts type JevAnswers = { [KEY in keyof QUESTIONS]: JevAnswer }; ``` Answers narrowed independently for every named question. #### Type Parameters | Type Parameter | | :------ | | `QUESTIONS` *extends* [`JevQuestions`](#jevquestions) | *** ### JevQuestion ```ts type JevQuestion = | YesNoQuestion | ChoiceQuestion | ScoreQuestion; ``` Any question accepted by `askJev`. *** ### JevQuestions ```ts type JevQuestions = Record; ``` Named decision questions evaluated against one shared state. *** ### JevResult ```ts type JevResult = object; ``` A typed decision result. #### Type Parameters | Type Parameter | | :------ | | `QUESTIONS` *extends* [`JevQuestions`](#jevquestions) | #### Properties | Property | Type | | :------ | :------ | | `answers` | [`JevAnswers`](#jevanswers)<`QUESTIONS`> | *** ### JevState ```ts type JevState = string | JevJsonObject | JevJsonValue[]; ``` JSON-compatible evidence evaluated by a decision model. *** ### JsonOnly ```ts type JsonOnly = { [K in keyof T as false extends IsData> ? never : K]: T[K] }; ``` The keys of a settings type whose values are data, so they can cross into a step. #### Type Parameters | Type Parameter | | :------ | | `T` | *** ### JsonValue ```ts type JsonValue = | string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue; }; ``` A value that can be serialized as JSON and embedded in a prompt or comment. *** ### LinearDefinition ```ts type LinearDefinition = z.input; ``` Who jigs is on Linear: `key` acts as the user whose `LINEAR_API_KEY` is in `.env`, `app` acts as a Linear OAuth application from `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET`. Defaults to `key`. #### Example ```ts linear: { identity: { mode: "app" } }, ``` *** ### McpHttpServerConfig ```ts type McpHttpServerConfig = object; ``` Configuration for an MCP server reached over HTTP. #### Properties | Property | Type | | :------ | :------ | | `headers?` | `Record`<`string`, `string`> | | `probe` | [`McpToolProbe`](#mcptoolprobe) | | `url` | `string` | *** ### McpServerConfig ```ts type McpServerConfig = | McpStdioServerConfig | McpHttpServerConfig; ``` An MCP server an agent harness can expose to the model. *** ### McpStdioServerConfig ```ts type McpStdioServerConfig = object; ``` Configuration for an MCP server launched as a child process. #### Properties | Property | Type | | :------ | :------ | | `args?` | `string`\[] | | `command` | `string` | | `env?` | `Record`<`string`, `string`> | | `probe` | [`McpToolProbe`](#mcptoolprobe) | *** ### McpToolProbe ```ts type McpToolProbe = object; ``` A harmless MCP tool call used to prove that a configured server is available. #### Properties | Property | Type | | :------ | :------ | | `arguments?` | `Record`<`string`, `unknown`> | | `tool` | `string` | *** ### MergeDefinition ```ts type MergeDefinition = z.input; ``` Who merges, by which of GitHub's three methods, and what signal permits it. *** ### MergePolicy ```ts type MergePolicy = z.output; ``` The effective pull request merge behavior for a binding. *** ### ModelKind ```ts type ModelKind = ModelSource["kind"]; ``` The stable name of a model source. *** ### ModelResult ```ts type ModelResult = object; ``` Text and structured output returned by a model call. #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `T` | `unknown` | #### Properties | Property | Type | | :------ | :------ | | `output` | `T` | | `text` | `string` | *** ### ModelSource ```ts type ModelSource = | OpenrouterSource | OpenaiCompatibleSource | OpenaiCodexSource; ``` Any configured source from which a model can answer. *** ### OpenaiCodexSource ```ts type OpenaiCodexSource = object; ``` The Codex subscription model source used only by the Pi harness. #### Properties | Property | Type | | :------ | :------ | | `kind` | `"openai-codex"` | | `model` | `string` | *** ### OpenaiCompatibleSource ```ts type OpenaiCompatibleSource = object; ``` An OpenAI-compatible API model source. #### Properties | Property | Type | | :------ | :------ | | `apiKeyEnv?` | `string` | | `baseUrl` | `string` | | `kind` | `"openai-compatible"` | | `model` | `string` | | `name` | `string` | *** ### OpenrouterSource ```ts type OpenrouterSource = object; ``` An OpenRouter API model source. #### Properties | Property | Type | | :------ | :------ | | `apiKeyEnv` | `string` | | `kind` | `"openrouter"` | | `model` | `string` | *** ### OutputJsonSchema ```ts type OutputJsonSchema = Record; ``` The serializable JSON Schema sent across the workflow-step boundary. *** ### PiHarness ```ts type PiHarness = | PiOpenaiCompatibleHarness | PiOtherHarness; ``` A Pi harness descriptor backed by a nested model source. *** ### PiHarnessOptions ```ts type PiHarnessOptions = Pick & object; ``` Options for `harnesses.pi`. `compat` applies only to an OpenAI-compatible model source. #### Type Declaration | Name | Type | | :------ | :------ | | `compat?` | `Partial`<[`PiOpenaiCompatibleOptions`](#piopenaicompatibleoptions)> | *** ### PiMcpHttpServerConfig ```ts type PiMcpHttpServerConfig = Omit & object & | { auth: "oauth"; bearerTokenEnv?: never; } | { auth?: false; bearerTokenEnv?: never; } | { auth?: never; bearerTokenEnv: string; }; ``` An HTTP MCP server Pi exposes through an explicit direct-tool allowlist. #### Type Declaration | Name | Type | Description | | :------ | :------ | :------ | | `headers?` | `Record`<`string`, `string`> | Maps HTTP header names to step-side source environment variable names. | | `tools` | `string`\[] | Raw MCP tool names the model may call. This must include the probe tool. | *** ### PiMcpServerConfig ```ts type PiMcpServerConfig = | PiMcpStdioServerConfig | PiMcpHttpServerConfig; ``` An explicitly configured MCP server accepted by the Pi harness. *** ### PiMcpStdioServerConfig ```ts type PiMcpStdioServerConfig = Omit & object; ``` A stdio MCP server Pi exposes through an explicit direct-tool allowlist. #### Type Declaration | Name | Type | Description | | :------ | :------ | :------ | | `env?` | `Record`<`string`, `string`> | Maps child variable names to step-side source environment variable names. | | `tools` | `string`\[] | Raw MCP tool names the model may call. This must include the probe tool. | *** ### PiOpenaiCompatibleHarness ```ts type PiOpenaiCompatibleHarness = SharedPiHarness & object; ``` A Pi harness descriptor backed by an OpenAI-compatible source, with its compatibility hints. #### Type Declaration | Name | Type | | :------ | :------ | | `compat` | [`PiOpenaiCompatibleOptions`](#piopenaicompatibleoptions) | | `model` | [`OpenaiCompatibleSource`](#openaicompatiblesource) | *** ### PiOpenaiCompatibleOptions ```ts type PiOpenaiCompatibleOptions = object; ``` Pi-specific compatibility hints for an OpenAI-compatible model. #### Properties | Property | Type | | :------ | :------ | | `supportsDeveloperRole` | `boolean` | | `supportsReasoningEffort` | `boolean` | *** ### PiOtherHarness ```ts type PiOtherHarness = SharedPiHarness & object; ``` A Pi harness descriptor backed by any source other than an OpenAI-compatible one. #### Type Declaration | Name | Type | | :------ | :------ | | `compat?` | `never` | | `model` | `Exclude`<[`ModelSource`](#modelsource), [`OpenaiCompatibleSource`](#openaicompatiblesource)> | *** ### PullRequestRef ```ts type PullRequestRef = object; ``` Identifies a pull request by repository owner, repository name and number. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `number` | `number` | The repository-local pull request number. | | `owner` | `string` | The GitHub organization or account that owns the repository. | | `repo` | `string` | The repository name. | *** ### PullRequestWake ```ts type PullRequestWake = | { headSha: string; kind: "merge-ready"; retryNoted: boolean; } | { body?: string; kind: "review-comments"; threads: ReviewThread[]; } | { failing: CheckRun[]; headSha: string; kind: "ci-red"; mentionLogin: string | null; } | { kind: "closed"; merged: boolean; }; ``` What is outstanding on the pull request right now. Every wake describes current state, so the same state yields the same wake until the consumer leaves evidence on the pull request that it is done with it: * `review-comments`: feedback with no answer carrying this scope's marker. * `ci-red`: the current head is red, with no marked stand-down for it. * `merge-ready`: GitHub reports the pull request mergeable and the configured approval signal is present, with no marked stand-down for it. `retryNoted` says a refusal jigs is waiting out was already reported for this head, so the retry is silent. * `closed`: terminal. #### Type Declaration ```ts { headSha: string; kind: "merge-ready"; retryNoted: boolean; } ``` | Name | Type | Description | | :------ | :------ | :------ | | `headSha` | `string` | The reviewed commit that the merge must still target. | | `kind` | `"merge-ready"` | Identifies a pull request that is ready for an attempted merge. | | `retryNoted` | `boolean` | Whether a transient refusal for this commit was already reported. | ```ts { body?: string; kind: "review-comments"; threads: ReviewThread[]; } ``` | Name | Type | Description | | :------ | :------ | :------ | | `body?` | `string` | The changes-requested review summary, when the feedback included one. | | `kind` | `"review-comments"` | Identifies unanswered review feedback. | | `threads` | [`ReviewThread`](#reviewthread)\[] | Inline and conversation threads that still need answers. | ```ts { failing: CheckRun[]; headSha: string; kind: "ci-red"; mentionLogin: string | null; } ``` | Name | Type | Description | | :------ | :------ | :------ | | `failing` | [`CheckRun`](#checkrun)\[] | Failed checks reported by the provider. | | `headSha` | `string` | The commit whose checks failed. | | `kind` | `"ci-red"` | Identifies a failed build on the current commit. | | `mentionLogin` | `string` | `null` | The most recent human reviewer to notify when repair cannot continue. | ```ts { kind: "closed"; merged: boolean; } ``` | Name | Type | Description | | :------ | :------ | :------ | | `kind` | `"closed"` | Identifies a terminal, closed pull request. | | `merged` | `boolean` | Whether the pull request closed by merging. | *** ### RebuildContextPrompt() ```ts type RebuildContextPrompt = (input) => string; ``` Renders instructions for rebuilding an agent's working context. #### Parameters | Parameter | Type | | :------ | :------ | | `input` | [`RebuildContextPromptInput`](#rebuildcontextpromptinput) | #### Returns `string` *** ### RebuildContextPromptInput ```ts type RebuildContextPromptInput = object; ``` Material a fresh agent needs to continue work after a session cannot resume. #### Properties | Property | Type | | :------ | :------ | | `brief` | `string` | | `diff` | `string` | | `threads` | `string` | | `ticket` | `string` | *** ### ReleasePolicy ```ts type ReleasePolicy = z.input; ``` Selects whether eligible run resources are released for each terminal outcome. *** ### RunAgentOptions ```ts type RunAgentOptions = object; ``` Options for an agent that works inside a directory. #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `T` | `undefined` | #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `cwd` | `string` | - | | `harness` | [`Harness`](#harness-2) | - | | `output?` | `z.ZodType`<`T`> | - | | `prompt` | `string` | - | | `resume?` | [`AgentSessionRef`](#agentsessionref) | The session reference of an earlier run to continue. | *** ### ScoreQuestion ```ts type ScoreQuestion = object; ``` A question scored over ordered levels, from lowest to highest. #### Properties | Property | Type | | :------ | :------ | | `instructions` | `string` | | `levels` | `string`\[] | | `type` | `"score"` | *** ### StatusReason ```ts type StatusReason = "merge" | "ci" | "merge-retry"; ``` Why a `status` note was written, so one note never silences another. `merge` and `ci` stand a commit down; `merge-retry` only records that the refusal was already reported, and leaves the commit merge-ready. *** ### TicketComment ```ts type TicketComment = object; ``` A Linear ticket comment captured in a workflow snapshot. #### Properties | Property | Type | | :------ | :------ | | `author` | `string` | `null` | | `body` | `string` | | `createdAt` | `string` | | `id` | `string` | *** ### TicketHandoff ```ts type TicketHandoff = object; ``` What a ticket review hands the builder: the brief plus the snapshot it was written from. Both travel together on purpose — the ticket is authoritative wherever the two conflict, and review or verify steps judge the work against the snapshot's acceptance criteria, never against the brief, so a re-planning agent cannot move the goalposts. `assumptions` is what the review decided for itself rather than asked about. It is posted to the ticket, so a human can still correct it. #### Properties | Property | Type | | :------ | :------ | | `assumptions` | `string`\[] | | `brief` | `string` | | `snapshot` | [`TicketSnapshot`](#ticketsnapshot) | *** ### TicketLink ```ts type TicketLink = object; ``` A named external link attached to a Linear ticket. #### Properties | Property | Type | | :------ | :------ | | `title` | `string` | | `url` | `string` | *** ### TicketNote ```ts type TicketNote = object; ``` A comment jigs posts on the ticket that asks for nothing and suspends nothing. It carries its own words, the way a halt does, so the renderer owns the layout and every caller owns what it says. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `closing` | `string` | What the reader should do with it. | | `headline` | `string` | One plain sentence naming what jigs is about to do, or has stopped doing. | | `notes` | `string`\[] | The bullet lines under it. | *** ### TicketRef ```ts type TicketRef = object; ``` A compact reference to a related Linear ticket. #### Properties | Property | Type | | :------ | :------ | | `id` | `string` | | `identifier` | `string` | | `title` | `string` | *** ### TicketReviewPrompt() ```ts type TicketReviewPrompt = (input) => string; ``` Renders instructions for an agent to turn a ticket into an actionable handoff. #### Parameters | Parameter | Type | | :------ | :------ | | `input` | [`TicketReviewPromptInput`](#ticketreviewpromptinput) | #### Returns `string` *** ### TicketReviewPromptInput ```ts type TicketReviewPromptInput = object; ``` The rendered ticket supplied to a ticket-review prompt. #### Properties | Property | Type | | :------ | :------ | | `ticket` | `string` | *** ### TicketSnapshot ```ts type TicketSnapshot = object; ``` The fixed ticket state shared by every step in one workflow activation. #### Properties | Property | Type | | :------ | :------ | | `blockedBy` | [`TicketRef`](#ticketref)\[] | | `blocks` | [`TicketRef`](#ticketref)\[] | | `branchName` | `string` | | `comments` | [`TicketComment`](#ticketcomment)\[] | | `description` | `string` | | `fetchedAt` | `string` | | `id` | `string` | | `identifier` | `string` | | `labels` | `string`\[] | | `links` | [`TicketLink`](#ticketlink)\[] | | `state` | `string` | | `subIssues` | [`TicketRef`](#ticketref)\[] | | `title` | `string` | | `url` | `string` | *** ### TicketWorkflowInputs ```ts type TicketWorkflowInputs = WorkflowInputs; ``` Ticket references are ordinary inputs; resolve them explicitly in a step. #### Type Parameters | Type Parameter | | :------ | | `S` *extends* `z.ZodType`<{ `ticket`: `string`; }> | *** ### ToolFree ```ts type ToolFree = object; ``` Marks a descriptor that names no tools or MCP servers. #### Properties | Property | Type | | :------ | :------ | | `mcpServers?` | `never` | | `tools?` | `never` | *** ### WebhooksDefinition ```ts type WebhooksDefinition = z.input; ``` Where provider webhooks reach the service, and which providers send them. Without this section the service still wakes parked runs by polling. #### Example ```ts webhooks: { url: "https://factory.example.ts.net", github: { enabled: true }, linear: { enabled: false }, }, ``` *** ### WorkflowInputs ```ts type WorkflowInputs = z.output & Injected; ``` Parsed workflow inputs with the trigger that started the run. #### Type Parameters | Type Parameter | | :------ | | `S` *extends* `z.ZodType` | *** ### YesNoQuestion ```ts type YesNoQuestion = object; ``` A calibrated yes-or-no question. #### Properties | Property | Type | | :------ | :------ | | `instructions` | `string` | | `type` | `"yes-no"` | ## Variables ### approvalSignalSchema ```ts const approvalSignalSchema: ZodDiscriminatedUnion<[ZodObject<{ kind: ZodLiteral<"review">; }, $strict>, ZodObject<{ kind: ZodLiteral<"label">; name: ZodString; }, $strict>], "kind">; ``` Selects how the operator authorizes an automatic merge. *** ### haltOptionSchema ```ts const haltOptionSchema: ZodObject<{ label: ZodString; recommended: ZodOptional; }, $strict>; ``` Validates an answer choice with a nonempty label and an optional recommendation marker. *** ### haltQuestionSchema ```ts const haltQuestionSchema: ZodObject<{ context: ZodOptional; options: ZodOptional; }, $strict>>>; question: ZodString; }, $strict>; ``` Validates a question with nonempty text, optional context and optional suggested answers. *** ### harnesses ```ts const harnesses: object; ``` Constructors for agent-harness descriptors. #### Type Declaration | Name | Type | Default value | | :------ | :------ | :------ | | `claude()` | <`O`>(`settings`) => [`HarnessForOptions`](#harnessforoptions)<[`ClaudeHarness`](#claudeharness), `O`> | `claudeHarness` | | `codex()` | <`O`>(`settings`) => [`CodexHarness`](#codexharness) | `codexHarness` | | `pi()` | { <`O`> (`model`, `options?`): [`HarnessForOptions`](#harnessforoptions)<[`PiOpenaiCompatibleHarness`](#piopenaicompatibleharness), `O`>; <`O`> (`model`, `options?`): [`HarnessForOptions`](#harnessforoptions)<[`PiOtherHarness`](#piotherharness), `O`>; <`M`, `O`> (`model`, `options?`): [`HarnessForOptions`](#harnessforoptions)<[`PiHarness`](#piharness), `O`>; } | `piHarness` | *** ### harnessKinds ```ts const harnessKinds: ["claude" | "codex" | "pi", ...("claude" | "codex" | "pi")[]]; ``` Every harness kind this release of jigs can build, taken from the keys of `harnesses`. Use it for a workflow input that names a harness, so a new kind appears without editing the input. #### Example ```ts const inputs = z.object({ harness: z.enum(harnessKinds) }); ``` *** ### mergePolicySchema ```ts const mergePolicySchema: ZodObject<{ approval: ZodDefault; }, $strict>, ZodObject<{ kind: ZodLiteral<"label">; name: ZodString; }, $strict>], "kind">>; by: ZodDefault>; method: ZodDefault>; }, $strict>; ``` Configures who merges a pull request, how it is merged and how approval is recorded. #### Remarks `by` chooses an automatic jigs merge or a human merge. `method` selects squash, merge-commit or rebase behavior. `approval` requires either a review of the current commit or a named label that remains valid after later pushes. *** ### models ```ts const models: object; ``` Constructors for model-source descriptors. #### Type Declaration | Name | Type | Description | | :------ | :------ | :------ | | `openaiCodex()` | (`model`) => [`OpenaiCodexSource`](#openaicodexsource) | Build a source that runs through the Codex subscription Pi is logged in to. Only `harnesses.pi` accepts it. | | `openaiCompatible()` | (`options`) => [`OpenaiCompatibleSource`](#openaicompatiblesource) | Build a source for an OpenAI-compatible server. | | `openrouter()` | (`model`, `options`) => [`OpenrouterSource`](#openroutersource) | Build an OpenRouter source. Its key is read from `OPENROUTER_API_KEY` unless `apiKeyEnv` names another variable. | *** ### rebuildContextPrompt ```ts const rebuildContextPrompt: RebuildContextPrompt; ``` The default prompt for continuing reviewed work in a fresh agent session. *** ### ticketInputSchema ```ts const ticketInputSchema: ZodUnion; ``` Accept a Linear issue UUID or an uppercase team-and-number ticket identifier. *** ### ticketReviewPrompt ```ts const ticketReviewPrompt: TicketReviewPrompt; ``` The default prompt for reviewing a Linear ticket before implementation begins. *** ### ticketReviewVerdictSchema ```ts const ticketReviewVerdictSchema: ZodObject<{ about: ZodString; assumptions: ZodArray; brief: ZodString; questions: ZodArray; options: ZodOptional; }, $strict>>>; question: ZodString; }, $strict>>; verdict: ZodEnum<{ needs-human: "needs-human"; proceed: "proceed"; }>; }, $strict>; ``` Structured verdict returned by the agent that reviews a ticket before work starts. ## Functions ### choice() ```ts function choice(instructions, options): ChoiceQuestion; ``` Build a question answered with one named option. #### Type Parameters | Type Parameter | | :------ | | `OPTIONS` *extends* `Record`<`string`, `string`> | #### Parameters | Parameter | Type | | :------ | :------ | | `instructions` | `string` | | `options` | `OPTIONS` | #### Returns [`ChoiceQuestion`](#choicequestion)<`OPTIONS`> *** ### defaultPullRequestScope() ```ts function defaultPullRequestScope(subject): string; ``` The scope a caller gets when it names none: this workflow's function name and the subject it was given — a ticket key, or the pull request itself. Pass an explicit scope to continue another workflow's work, or to review a pull request independently of the run delivering it. #### Parameters | Parameter | Type | | :------ | :------ | | `subject` | `string` | #### Returns `string` *** ### defineFactory() ```ts function defineFactory(factory): T; ``` Preserve the declaration's inferred keys without loading its workflows. #### Type Parameters | Type Parameter | | :------ | | `T` *extends* [`FactoryDefinition`](#factorydefinition) | #### Parameters | Parameter | Type | | :------ | :------ | | `factory` | `T` | #### Returns `T` *** ### defineWorkflow() ```ts function defineWorkflow(definition): WorkflowDefinition; ``` Declare a workflow as the default export of its file. It returns the definition unchanged; it exists so TypeScript checks the workflow's parameter against the input schema. #### Type Parameters | Type Parameter | | :------ | | `S` *extends* `ZodType`<`unknown`, `unknown`, `$ZodTypeInternals`<`unknown`, `unknown`>> | #### Parameters | Parameter | Type | | :------ | :------ | | `definition` | [`WorkflowDefinition`](#workflowdefinition)<`S`> | #### Returns [`WorkflowDefinition`](#workflowdefinition)<`S`> #### Example ```ts const inputs = z.object({ binding: z.string() }); export async function hello(input: WorkflowInputs) { "use workflow"; // ... } export default defineWorkflow({ inputs, workflow: hello }); ``` *** ### describeHarness() ```ts function describeHarness(harness): string; ``` A harness descriptor as a string that ignores field order: two descriptors that list the same settings in another order render the same. #### Parameters | Parameter | Type | | :------ | :------ | | `harness` | [`Harness`](#harness-2) | #### Returns `string` *** ### interpolate() ```ts function interpolate(template, values): string; ``` Replace named `{{ placeholders }}` once, leaving unknown names unchanged. #### Parameters | Parameter | Type | | :------ | :------ | | `template` | `string` | | `values` | `Record`<`string`, `string`> | #### Returns `string` *** ### isPullRequestMergeReady() ```ts function isPullRequestMergeReady(snapshot, approval): boolean; ``` Whether current GitHub facts satisfy the configured approval and merge requirements. #### Parameters | Parameter | Type | | :------ | :------ | | `snapshot` | [`PullRequestSnapshot`](#pullrequestsnapshot) | | `approval` | | { `kind`: `"review"`; } | { `kind`: `"label"`; `name`: `string`; } | #### Returns `boolean` *** ### parseMarkers() ```ts function parseMarkers(body): PullRequestMarker[]; ``` Every marker in one comment body, in the order they appear. #### Parameters | Parameter | Type | | :------ | :------ | | `body` | `string` | #### Returns [`PullRequestMarker`](#pullrequestmarker)\[] *** ### pullRequestSnapshotKey() ```ts function pullRequestSnapshotKey(snapshot): string; ``` A comparison key for the facts in a pull request snapshot. #### Parameters | Parameter | Type | | :------ | :------ | | `snapshot` | [`PullRequestSnapshot`](#pullrequestsnapshot) | #### Returns `string` #### Remarks Collection ordering and incidental fields do not change the key. Compare keys for equality; the key format is opaque and is not a durable identifier. *** ### renderChangeSummary() ```ts function renderChangeSummary(summary): string; ``` Render a Markdown review summary with commits, totals and up to 60 changed-file rows. #### Parameters | Parameter | Type | | :------ | :------ | | `summary` | [`ChangeSummary`](#changesummary) | #### Returns `string` *** ### renderChecks() ```ts function renderChecks(failing): string; ``` Render failed checks as a Markdown list for a pull request note. #### Parameters | Parameter | Type | | :------ | :------ | | `failing` | [`CheckRun`](#checkrun)\[] | #### Returns `string` *** ### renderTicketSnapshot() ```ts function renderTicketSnapshot(snapshot): string; ``` Render a ticket snapshot as Markdown for an agent prompt. #### Parameters | Parameter | Type | | :------ | :------ | | `snapshot` | [`TicketSnapshot`](#ticketsnapshot) | #### Returns `string` *** ### score() ```ts function score(instructions, levels): ScoreQuestion; ``` Build a question scored over ordered levels, from lowest to highest. #### Parameters | Parameter | Type | | :------ | :------ | | `instructions` | `string` | | `levels` | `string`\[] | #### Returns [`ScoreQuestion`](#scorequestion) *** ### unreachable() ```ts function unreachable(value): never; ``` Fail an exhaustive branch if an unexpected value reaches it at runtime. #### Parameters | Parameter | Type | | :------ | :------ | | `value` | `never` | #### Returns `never` *** ### yesNo() ```ts function yesNo(instructions): YesNoQuestion; ``` Build a calibrated yes-or-no question. #### Parameters | Parameter | Type | | :------ | :------ | | `instructions` | `string` | #### Returns [`YesNoQuestion`](#yesnoquestion) ## Factory plumbing ### JitCheckError A failed just-in-time tool check, with repair details for each failure. #### Extends * `Error` #### Constructors ##### Constructor ```ts new JitCheckError(failures): JitCheckError; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `failures` | `object` & `object`\[] | ###### Returns [`JitCheckError`](#jitcheckerror) ###### Overrides ```ts Error.constructor ``` #### Properties | Property | Type | | :------ | :------ | | `failures` | `object` & `object`\[] | *** ### AgentRequest ```ts type AgentRequest = | Omit & object | Omit & object; ``` Serializable agent request passed to a durable step. *** ### ModelRequest ```ts type ModelRequest = Omit & object; ``` Serializable API model request passed to a durable step. #### Type Declaration | Name | Type | | :------ | :------ | | `outputSchema?` | [`OutputJsonSchema`](#outputjsonschema) | *** ### unwrapAgentStep() ```ts function unwrapAgentStep(result): AgentResult; ``` Convert returned execution failure markers into errors the workflow throws. #### Parameters | Parameter | Type | | :------ | :------ | | `result` | | [`AgentResult`](#agentresult) | { `jitFailure`: `object` & `object`\[]; } | { `resumeFailed`: `string`; } | #### Returns [`AgentResult`](#agentresult) --- --- url: https://salimhamed.github.io/jigs/api/steps/agents.md --- # steps/agents Execute agent and model requests outside workflow code. Wrap steps in a factory-owned `"use step"` file. Never call them directly from a workflow. ## Functions ### executeAgent() ```ts function executeAgent(wire, metadata): Promise< | AgentResult | { jitFailure: object & object[]; } | { resumeFailed: string; }>; ``` Run or ask an agent harness, checking worktree requirements before a run. #### Parameters | Parameter | Type | | :------ | :------ | | `wire` | [`AgentRequest`](../jigs.md#agentrequest) | | `metadata` | [`RunMetadata`](../steps.md#runmetadata) | #### Returns `Promise`< | [`AgentResult`](../jigs.md#agentresult) | { `jitFailure`: `object` & `object`\[]; } | { `resumeFailed`: `string`; }> *** ### executeJev() ```ts function executeJev(wire, metadata): Promise>; ``` Evaluate typed questions with a decision-capable model. #### Type Parameters | Type Parameter | | :------ | | `QUESTIONS` *extends* [`JevQuestions`](../jigs.md#jevquestions) | #### Parameters | Parameter | Type | | :------ | :------ | | `wire` | [`AskJevOptions`](../jigs.md#askjevoptions)<`QUESTIONS`> | | `metadata` | [`RunMetadata`](../steps.md#runmetadata) | #### Returns `Promise`<[`JevResult`](../jigs.md#jevresult)<`QUESTIONS`>> *** ### executeModel() ```ts function executeModel(wire, metadata): Promise; ``` Ask an API-backed model source. #### Parameters | Parameter | Type | | :------ | :------ | | `wire` | [`ModelRequest`](../jigs.md#modelrequest) | | `metadata` | [`RunMetadata`](../steps.md#runmetadata) | #### Returns `Promise`<[`ModelResult`](../jigs.md#modelresult)> --- --- url: https://salimhamed.github.io/jigs/api/steps/git.md --- # steps/git Inspect committed changes and push branches in a Git worktree. Wrap steps in a factory-owned `"use step"` file. Never call them directly from a workflow. ## Functions ### branchContains() ```ts function branchContains(worktreePath, sha): Promise; ``` Whether `sha` is the worktree's HEAD or one of its ancestors. A commit the worktree has never fetched is not contained. #### Parameters | Parameter | Type | | :------ | :------ | | `worktreePath` | `string` | | `sha` | `string` | #### Returns `Promise`<`boolean`> *** ### pushApprovedChange() ```ts function pushApprovedChange( worktreePath, branch, approvedCommit): Promise<{ headSha: string; }>; ``` Push a reviewed commit only while it is still HEAD and the worktree is clean. Safe to retry after a successful push. Rejects if HEAD moved or any uncommitted change exists. #### Parameters | Parameter | Type | | :------ | :------ | | `worktreePath` | `string` | | `branch` | `string` | | `approvedCommit` | `string` | #### Returns `Promise`<{ `headSha`: `string`; }> *** ### pushBranch() ```ts function pushBranch(worktreePath, branch): Promise<{ headSha: string; }>; ``` Push the worktree's current HEAD and register a GitHub branch resource when applicable. #### Parameters | Parameter | Type | | :------ | :------ | | `worktreePath` | `string` | | `branch` | `string` | #### Returns `Promise`<{ `headSha`: `string`; }> *** ### readBranchState() ```ts function readBranchState(worktreePath, baseSha): Promise<{ commits: number; dirty: boolean; headSha: string; }>; ``` Inspect the worktree state used to decide whether a branch is ready to push. #### Parameters | Parameter | Type | | :------ | :------ | | `worktreePath` | `string` | | `baseSha` | `string` | #### Returns `Promise`<{ `commits`: `number`; `dirty`: `boolean`; `headSha`: `string`; }> *** ### readChange() ```ts function readChange(worktreePath, base): Promise; ``` Describe committed changes between a base ref and the worktree's current HEAD. #### Parameters | Parameter | Type | | :------ | :------ | | `worktreePath` | `string` | | `base` | `string` | #### Returns `Promise`<[`ChangeSummary`](../jigs.md#changesummary)> #### Remarks Resolves both endpoints once, compares their trees directly and lists commits reachable only from HEAD. Returns at most 1,000 files and 1,000 commits; `truncated` reports omitted results. *** ### readPatch() ```ts function readPatch( worktreePath, base, head, paths): Promise; ``` Read patches for selected literal paths between two commits. #### Parameters | Parameter | Type | | :------ | :------ | | `worktreePath` | `string` | | `base` | `string` | | `head` | `string` | | `paths` | `string`\[] | #### Returns `Promise`<[`ChangePatch`](../jigs.md#changepatch)> #### Remarks Pass the resolved `base` and `head` from `readChange` to inspect that exact change. Paths are deduplicated, empty paths are rejected and all returned patches share a 200,000-character limit. *** ### readWorktreeDiff() ```ts function readWorktreeDiff(worktreePath, baseSha): Promise; ``` Read a raw patch from the merge base of `baseSha` and HEAD, truncating after 200,000 characters. #### Parameters | Parameter | Type | | :------ | :------ | | `worktreePath` | `string` | | `baseSha` | `string` | #### Returns `Promise`<`string`> --- --- url: https://salimhamed.github.io/jigs/api/steps/human.md --- # steps/human Reserved for provider-neutral human interaction steps. Wrap steps in a factory-owned `"use step"` file. Never call them directly from a workflow. --- --- url: https://salimhamed.github.io/jigs/api/steps/linear.md --- # steps/linear Read and update Linear issues outside workflow code. Wrap steps in a factory-owned `"use step"` file. Never call them directly from a workflow. ## Interfaces ### CreateIssueInProjectInput Fields used to create a Linear ticket in a project's first team. #### Properties | Property | Type | | :------ | :------ | | `description` | `string` | | `project` | `string` | | `title` | `string` | *** ### LinearIssueMatch A matching Linear ticket returned by a project title search. #### Properties | Property | Type | | :------ | :------ | | `description` | `string` | | `id` | `string` | | `identifier` | `string` | | `state` | `string` | | `title` | `string` | | `url` | `string` | *** ### TicketStatusResult The before-and-after state names from a ticket status update. #### Properties | Property | Type | | :------ | :------ | | `changed` | `boolean` | | `from` | `string` | | `to` | `string` | ## Type Aliases ### NeedsHumanContext ```ts type NeedsHumanContext = object; ``` What the comment's footer says about the run that posted it. The factory's step wrapper builds it: the run id and the workflow name come from the Workflow SDK's metadata, and the dashboard link from the service's own configuration — none of it visible to the workflow. Where the run paused is the halt's, not the context's: only the routine that raised it knows. #### Properties | Property | Type | | :------ | :------ | | `dashboardUrl?` | `string` | | `runId` | `string` | | `workflow?` | `string` | *** ### RenderNeedsHumanComment() ```ts type RenderNeedsHumanComment = (halt, context, participants) => string; ``` Renders the Linear comment that asks a person to unblock a run. #### Parameters | Parameter | Type | | :------ | :------ | | `halt` | [`Halt`](../jigs.md#halt) | | `context` | [`NeedsHumanContext`](#needshumancontext) | | `participants` | [`TicketParticipants`](#ticketparticipants) | #### Returns `string` *** ### RenderTicketNote() ```ts type RenderTicketNote = (note, participants) => string; ``` Renders a non-blocking Linear note for ticket participants. #### Parameters | Parameter | Type | | :------ | :------ | | `note` | [`TicketNote`](../jigs.md#ticketnote) | | `participants` | [`TicketParticipants`](#ticketparticipants) | #### Returns `string` *** ### TicketParticipants ```ts type TicketParticipants = object; ``` Who the comment greets. Either may be absent, and they are often the same. #### Properties | Property | Type | | :------ | :------ | | `assignee` | `LinearUser` | `null` | | `creator` | `LinearUser` | `null` | ## Variables ### checkForTicketHumanReply ```ts const checkForTicketHumanReply: CheckForTicketHumanReply; ``` Look for a reply since the last check, excluding every comment the run posted. *** ### renderNeedsHumanComment ```ts const renderNeedsHumanComment: RenderNeedsHumanComment; ``` Render the default human-input request as Linear Markdown. *** ### renderTicketNote ```ts const renderTicketNote: RenderTicketNote; ``` Render the default non-blocking ticket note as Linear Markdown. ## Functions ### createComment() ```ts function createComment(issueId, body): Promise<{ createdAt: string; id: string; }>; ``` Post a comment on a ticket. #### Parameters | Parameter | Type | | :------ | :------ | | `issueId` | `string` | | `body` | `string` | #### Returns `Promise`<{ `createdAt`: `string`; `id`: `string`; }> *** ### createIssueInProject() ```ts function createIssueInProject(input): Promise<{ id: string; identifier: string; url: string; }>; ``` Create a ticket in the project’s first team. #### Parameters | Parameter | Type | | :------ | :------ | | `input` | [`CreateIssueInProjectInput`](#createissueinprojectinput) | #### Returns `Promise`<{ `id`: `string`; `identifier`: `string`; `url`: `string`; }> *** ### fetchTicketSnapshot() ```ts function fetchTicketSnapshot(issueId): Promise; ``` Read the ticket’s current details and discussion. #### Parameters | Parameter | Type | | :------ | :------ | | `issueId` | `string` | #### Returns `Promise`<[`TicketSnapshot`](../jigs.md#ticketsnapshot)> *** ### findIssueInProject() ```ts function findIssueInProject(input): Promise; ``` Find the newest ticket in a project whose title starts with the given text. #### Parameters | Parameter | Type | | :------ | :------ | | `input` | { `project`: `string`; `titlePrefix`: `string`; } | | `input.project` | `string` | | `input.titlePrefix` | `string` | #### Returns `Promise`<[`LinearIssueMatch`](#linearissuematch) | `null`> *** ### postTicketHumanInputRequest() ```ts function postTicketHumanInputRequest( issueId, halt, metadata, render): Promise<{ commentId: string; postedAt: string; }>; ``` Post a question or failure on the ticket so a person can help the run continue. #### Parameters | Parameter | Type | Default value | | :------ | :------ | :------ | | `issueId` | `string` | `undefined` | | `halt` | [`Halt`](../jigs.md#halt) | `undefined` | | `metadata` | `NamedRunMetadata` | `undefined` | | `render` | [`RenderNeedsHumanComment`](#renderneedshumancomment) | `renderNeedsHumanComment` | #### Returns `Promise`<{ `commentId`: `string`; `postedAt`: `string`; }> *** ### postTicketNote() ```ts function postTicketNote( issueId, note, render): Promise<{ commentId: string; }>; ``` Tell ticket participants something the run decided, without waiting for a reply. #### Parameters | Parameter | Type | Default value | | :------ | :------ | :------ | | `issueId` | `string` | `undefined` | | `note` | [`TicketNote`](../jigs.md#ticketnote) | `undefined` | | `render` | [`RenderTicketNote`](#renderticketnote) | `renderTicketNote` | #### Returns `Promise`<{ `commentId`: `string`; }> *** ### resolveLinearIssue() ```ts function resolveLinearIssue(reference): Promise; ``` Resolve a Linear identifier or issue ID before claiming or reading the ticket. #### Parameters | Parameter | Type | | :------ | :------ | | `reference` | `string` | #### Returns `Promise`<`LinearIssueRef`> *** ### setTicketStatus() ```ts function setTicketStatus(issueId, stateName): Promise; ``` Set a ticket to one of its team's named states. #### Parameters | Parameter | Type | | :------ | :------ | | `issueId` | `string` | | `stateName` | `string` | #### Returns `Promise`<[`TicketStatusResult`](#ticketstatusresult)> --- --- url: https://salimhamed.github.io/jigs/api/steps/pull-requests.md --- # steps/pull-requests Read and update GitHub pull requests outside workflow code. Wrap steps in a factory-owned `"use step"` file. Never call them directly from a workflow. ## Interfaces ### GitHubRepoRef Identifies a GitHub repository by its owner and name. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `owner` | `string` | The GitHub organization or account that owns the repository. | | `repo` | `string` | The repository name. | ## Type Aliases ### MergeOutcome ```ts type MergeOutcome = | { mergeCommitSha: string | null; merged: true; } | object & MergeRefusal; ``` What GitHub did, and when it did not, why — and whether asking again could change the answer, which is what decides between standing the commit down and leaving it merge-ready. #### Type Declaration ```ts { mergeCommitSha: string | null; merged: true; } ``` | Name | Type | Description | | :------ | :------ | :------ | | `mergeCommitSha` | `string` | `null` | The merge commit, or `null` when GitHub has not reported it yet. | | `merged` | `true` | Confirms that GitHub reports the pull request merged. | `object` & `MergeRefusal` *** ### OpenedPullRequest ```ts type OpenedPullRequest = PullRequestRef & object; ``` A newly opened or adopted pull request and its browser URL. #### Type Declaration | Name | Type | Description | | :------ | :------ | :------ | | `url` | `string` | The pull request's browser URL. | ## Variables ### fetchPullRequestState ```ts const fetchPullRequestState: FetchPrState; ``` Read the pull request’s checks, reviews, and open review threads. ## Functions ### commentOnPullRequest() ```ts function commentOnPullRequest(pr, body): Promise<{ id: number; }>; ``` Post a comment on the pull request conversation and return its id. #### Parameters | Parameter | Type | | :------ | :------ | | `pr` | `PullRequestRef` | | `body` | `string` | #### Returns `Promise`<{ `id`: `number`; }> *** ### markPullRequestReady() ```ts function markPullRequestReady(pr): Promise; ``` Mark a draft pull request ready and return its freshly read state. #### Parameters | Parameter | Type | | :------ | :------ | | `pr` | `PullRequestRef` | #### Returns `Promise`<[`PullRequestSnapshot`](../jigs.md#pullrequestsnapshot)> *** ### mergePullRequest() ```ts function mergePullRequest( pr, expectedHeadSha, policy): Promise; ``` Merge the pull request with the configured method, pinned to the head the caller judged ready. The title is re-read here rather than carried in from `describePullRequest`: a reviewer who corrects it — to satisfy a conventional-commit check on the target repo, usually — does so between the pull request opening and this merge, and a title captured at open time would ship the one they corrected away. After any ambiguous answer the pull request is read again, and this reports `merged` only if GitHub says so. #### Parameters | Parameter | Type | Description | | :------ | :------ | :------ | | `pr` | `PullRequestRef` | - | | `expectedHeadSha` | `string` | - | | `policy` | { `approval`: | { `kind`: `"review"`; } | { `kind`: `"label"`; `name`: `string`; }; `by`: `"jigs"` | `"human"`; `method`: `"squash"` | `"merge"` | `"rebase"`; } | - | | `policy.approval` | | { `kind`: `"review"`; } | { `kind`: `"label"`; `name`: `string`; } | The signal that authorizes an automatic merge. | | `policy.by` | `"jigs"` | `"human"` | Whether jigs merges an eligible pull request or waits for a person to merge it. | | `policy.method` | `"squash"` | `"merge"` | `"rebase"` | The GitHub merge method to use when jigs performs the merge. | #### Returns `Promise`<[`MergeOutcome`](#mergeoutcome)> *** ### openPullRequest() ```ts function openPullRequest(request): Promise; ``` Open a pull request from the working branch into the base branch. The lookup comes first because this is one step: a create that succeeded before the assignment failed, or whose response was lost, leaves a pull request GitHub will refuse to open twice. The retry adopts that pull request and re-attempts only what did not finish. #### Parameters | Parameter | Type | | :------ | :------ | | `request` | { `base`: `string`; `body`: `string`; `draft?`: `boolean`; `head`: `string`; `repo`: [`GitHubRepoRef`](#githubreporef); `title`: `string`; } | | `request.base` | `string` | | `request.body` | `string` | | `request.draft?` | `boolean` | | `request.head` | `string` | | `request.repo` | [`GitHubRepoRef`](#githubreporef) | | `request.title` | `string` | #### Returns `Promise`<[`OpenedPullRequest`](#openedpullrequest)> *** ### replyToPullRequestReviewThread() ```ts function replyToPullRequestReviewThread( pr, rootId, body): Promise<{ id: number; }>; ``` Reply to a review thread and return the posted comment id. #### Parameters | Parameter | Type | | :------ | :------ | | `pr` | `PullRequestRef` | | `rootId` | `number` | | `body` | `string` | #### Returns `Promise`<{ `id`: `number`; }> *** ### resolveMergePolicy() ```ts function resolveMergePolicy(binding): Promise<{ approval: | { kind: "review"; } | { kind: "label"; name: string; }; by: "jigs" | "human"; method: "squash" | "merge" | "rebase"; }>; ``` Read the effective merge policy for a factory binding. #### Parameters | Parameter | Type | | :------ | :------ | | `binding` | `string` | #### Returns `Promise`<{ `approval`: | { `kind`: `"review"`; } | { `kind`: `"label"`; `name`: `string`; }; `by`: `"jigs"` | `"human"`; `method`: `"squash"` | `"merge"` | `"rebase"`; }> *** ### resolveRepository() ```ts function resolveRepository(binding): Promise; ``` Find the GitHub repository configured for a binding. #### Parameters | Parameter | Type | | :------ | :------ | | `binding` | `string` | #### Returns `Promise`<[`GitHubRepoRef`](#githubreporef)> *** ### reviewPullRequest() ```ts function reviewPullRequest(pr, review): Promise<{ id: number; }>; ``` Post a pull request review and return its id. GitHub refuses an approval from the pull request's own author with 422 Unprocessable Entity; Jigs lets GitHub's GithubApiError surface unchanged. #### Parameters | Parameter | Type | | :------ | :------ | | `pr` | `PullRequestRef` | | `review` | `PullRequestReviewRequest` | #### Returns `Promise`<{ `id`: `number`; }> --- --- url: https://salimhamed.github.io/jigs/api/steps/runtime.md --- # steps/runtime Read run context and update run resources outside workflow code. Wrap steps in a factory-owned `"use step"` file. Never call them directly from a workflow. ## Functions ### createRunDirectory() ```ts function createRunDirectory(metadata): Promise; ``` Create a working directory that survives retries and pauses in this run. #### Parameters | Parameter | Type | | :------ | :------ | | `metadata` | [`RunMetadata`](../steps.md#runmetadata) | #### Returns `Promise`<`string`> *** ### dashboardRunUrl() ```ts function dashboardRunUrl(runId): string | undefined; ``` The run's page on the dashboard this service hosts, or undefined when the service was started without one. Never a standalone `workflow web` URL: run against a live World it opens a second queue worker and steals the jobs the run is waiting on. #### Parameters | Parameter | Type | | :------ | :------ | | `runId` | `string` | #### Returns `string` | `undefined` *** ### registerResource() ```ts function registerResource(resource): Promise; ``` Register one resource on the active run. Repeating kind + identity is idempotent. A new URL for that identity replaces the old URL; concurrent updates are last-committed-wins. Distinct identities occupy distinct atomic keys. #### Parameters | Parameter | Type | | :------ | :------ | | `resource` | [`RunResource`](../jigs.md#runresource) | #### Returns `Promise`<[`RunResource`](../jigs.md#runresource)> *** ### releaseRunResources() ```ts function releaseRunResources(policy, metadata): Promise; ``` Persist an explicit success action, release under the run lock and return the result. #### Parameters | Parameter | Type | Description | | :------ | :------ | :------ | | `policy` | { `onFailure`: `"release"` | `"keep"`; `onSuccess`: `"release"` | `"keep"`; } | - | | `policy.onFailure` | `"release"` | `"keep"` | What to do with eligible resources after a failed or cancelled run. | | `policy.onSuccess` | `"release"` | `"keep"` | What to do with eligible resources after a completed run. | | `metadata` | [`RunMetadata`](../steps.md#runmetadata) | - | #### Returns `Promise`<[`ReleaseReport`](../jigs.md#releasereport)> *** ### removeRunDirectory() ```ts function removeRunDirectory(metadata): Promise; ``` Remove this run's working directory after its work is finished, never while paused. #### Parameters | Parameter | Type | | :------ | :------ | | `metadata` | [`RunMetadata`](../steps.md#runmetadata) | #### Returns `Promise`<`void`> *** ### resolveReleasePolicy() ```ts function resolveReleasePolicy(metadata, definition): Promise<{ onFailure: "release" | "keep"; onSuccess: "release" | "keep"; }>; ``` Resolve the workflow policy, then the factory policy, then the built-in release/keep default. #### Parameters | Parameter | Type | | :------ | :------ | | `metadata` | `NamedRunMetadata` | | `definition` | [`FactoryDefinition`](../jigs.md#factorydefinition) | #### Returns `Promise`<{ `onFailure`: `"release"` | `"keep"`; `onSuccess`: `"release"` | `"keep"`; }> --- --- url: https://salimhamed.github.io/jigs/api/steps/workspaces.md --- # steps/workspaces Provision a repository worktree outside workflow code. Wrap steps in a factory-owned `"use step"` file. Never call them directly from a workflow. ## Interfaces ### ProvisionWorktreeDependencies Injectable registry and ownership operations used while provisioning a worktree. #### Properties | Property | Type | | :------ | :------ | | `readOwner?` | (`runId`) => `Promise`<`OwnerState`> | | `sql?` | `RegistrySql` | | `withLock?` | <`T`>(`runId`, `action`) => `Promise`<`T`> | *** ### WorktreeRequest The binding and branch used to provision a run's worktree. #### Properties | Property | Type | | :------ | :------ | | `binding` | `string` | | `branch` | `string` | ## Functions ### provisionWorktree() ```ts function provisionWorktree( request, metadata, deps): Promise; ``` Create or reuse a worktree for this run and prepare its files and dependencies. #### Parameters | Parameter | Type | | :------ | :------ | | `request` | [`WorktreeRequest`](#worktreerequest) | | `metadata` | [`RunMetadata`](../steps.md#runmetadata) | | `deps` | [`ProvisionWorktreeDependencies`](#provisionworktreedependencies) | #### Returns `Promise`<[`Worktree`](../jigs.md#worktree)> --- --- url: https://salimhamed.github.io/jigs/api/steps.md --- # steps Build a factory's own agent step. `createAgentRunner` opens a harness the way the built-in agent step does and hands back the live provider model. Call these inside a factory-owned `"use step"` function, never from a workflow. `Driver`, `DriverContext`, `AgentRunner` and the types they reach are a published contract: a change to any of them is a breaking release. ## Classes ### AgentSessionError A durable agent session is missing or cannot be resumed by this harness. #### Extends * `Error` #### Properties | Property | Type | Default value | | :------ | :------ | :------ | | `name` | `"AgentSessionError"` | `"AgentSessionError"` | ## Interfaces ### AgentRunner A harness ready to run in a worktree, from [createAgentRunner](#createagentrunner). Pass `model` to the AI SDK's `generateText`, read the session reference from its result with `sessionFrom`, and call `close` when the call is done. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `model` | `LanguageModel` | The live provider model, with jigs' policy already applied. | #### Methods ##### close() ```ts close(): Promise; ``` Release the worktree lock and stop what the harness started. Safe to call twice. ###### Returns `Promise`<`void`> ##### sessionFrom() ```ts sessionFrom(result): AgentSessionRef | undefined; ``` The session reference in a `generateText` result, for a later step to resume. ###### Parameters | Parameter | Type | | :------ | :------ | | `result` | { `providerMetadata?`: `Record`<`string`, `Record`<`string`, `unknown`>> | `null`; } | | `result.providerMetadata?` | `Record`<`string`, `Record`<`string`, `unknown`>> | `null` | ###### Returns [`AgentSessionRef`](jigs.md#agentsessionref) | `undefined` *** ### AgentRunnerOptions Where [createAgentRunner](#createagentrunner) runs a harness, and the session it resumes. #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `cwd` | `string` | The worktree the agent works in. | | `resume?` | [`AgentSessionRef`](jigs.md#agentsessionref) | A session reference from an earlier call to resume. | | `run` | [`RunMetadata`](#runmetadata) | The run the step belongs to: `getWorkflowMetadata()` inside the step. | *** ### Check One requirement check with a stable id and a label for reports. #### Properties | Property | Type | | :------ | :------ | | `id` | `string` | | `label` | `string` | #### Methods ##### run() ```ts run(): Promise; ``` ###### Returns `Promise`<[`CheckResult`](#checkresult)> *** ### Driver How jigs runs one harness or model-source kind: its checks, the environment it may see, and how it asks, runs or opens a provider model. Each kind a descriptor can name has exactly one driver inside jigs; a factory cannot register another. #### Remarks A factory reads this to know what `createAgentRunner` does before it hands back a model. The shape is a published contract: changing it is a breaking release. #### Type Parameters | Type Parameter | | :------ | | `K` *extends* [`HarnessKind`](jigs.md#harnesskind) | [`ModelKind`](jigs.md#modelkind) | #### Properties | Property | Type | Description | | :------ | :------ | :------ | | `displayName` | `string` | - | | `family` | `K` *extends* `"claude"` | `"codex"` | `"pi"` ? `"harness"` : `"model"` | - | | `kind` | `K` | - | | `minimumVersion?` | `string` | - | | `sessionPointer?` | `object` | - | | `sessionPointer.field` | `string` | - | | `sessionPointer.providerKey` | `string` | - | | `setsEnv` | readonly `string`\[] | Names the driver sets in the harness environment itself, such as a private home. | #### Methods ##### ask()? ```ts optional ask(request, context): Promise; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `request` | | [`AgentRequest`](jigs.md#agentrequest) | [`ModelRequest`](jigs.md#modelrequest) | | `context` | [`DriverContext`](#drivercontext) | ###### Returns `Promise`<[`ExecutorGeneration`](#executorgeneration)> ##### decide()? ```ts optional decide(request, context): Promise>; ``` ###### Type Parameters | Type Parameter | | :------ | | `QUESTIONS` *extends* [`JevQuestions`](jigs.md#jevquestions) | ###### Parameters | Parameter | Type | | :------ | :------ | | `request` | [`AskJevOptions`](jigs.md#askjevoptions)<`QUESTIONS`> | | `context` | [`DriverContext`](#drivercontext) | ###### Returns `Promise`<[`DecisionGeneration`](#decisiongeneration)<`QUESTIONS`>> ##### descriptorChecks()? ```ts optional descriptorChecks(source): Check[]; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `source` | | `Extract`<[`OpenrouterSource`](jigs.md#openroutersource), { `kind`: `K`; }> | `Extract`<[`OpenaiCompatibleSource`](jigs.md#openaicompatiblesource), { `kind`: `K`; }> | `Extract`<[`OpenaiCodexSource`](jigs.md#openaicodexsource), { `kind`: `K`; }> | ###### Returns [`Check`](#check)\[] ##### envAllowlist() ```ts envAllowlist(request): readonly string[]; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `request` | [`DriverRequest`](#driverrequest) | ###### Returns readonly `string`\[] ##### installationChecks() ```ts installationChecks(): Check[]; ``` ###### Returns [`Check`](#check)\[] ##### jitChecks()? ```ts optional jitChecks(target): Check[]; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `target` | [`HarnessTarget`](#harnesstarget) | ###### Returns [`Check`](#check)\[] ##### open()? ```ts optional open(target, context): Promise; ``` Build the live provider model for a run. Drivers without a provider model implement `run`. ###### Parameters | Parameter | Type | | :------ | :------ | | `target` | [`HarnessTarget`](#harnesstarget) | | `context` | [`OpenContext`](#opencontext) | ###### Returns `Promise`<[`OpenedModel`](#openedmodel)> ##### requestChecks() ```ts requestChecks(request): Check[]; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `request` | [`DriverRequest`](#driverrequest) | ###### Returns [`Check`](#check)\[] ##### resolveExecutable()? ```ts optional resolveExecutable(env): string; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `env` | `ProcessEnv` | ###### Returns `string` ##### run()? ```ts optional run(request, context): Promise; ``` ###### Parameters | Parameter | Type | | :------ | :------ | | `request` | `Omit`<[`RunAgentOptions`](jigs.md#runagentoptions)<`undefined`>, `"output"`> & `object` | | `context` | [`DriverContext`](#drivercontext) | ###### Returns `Promise`<[`ExecutorGeneration`](#executorgeneration)> *** ### DriverContext What a driver receives for one call: the run it belongs to, the harness environment jigs built for it, and, for a structured call, the output spec a provider model consumes. `deps` is jigs' own wiring, not part of the contract. #### Properties | Property | Type | | :------ | :------ | | `env` | `Record`<`string`, `string`> | | `metadata` | [`RunMetadata`](#runmetadata) | | `output?` | `Output`<`unknown`, `unknown`, `never`> | *** ### OpenContext What a driver's `open` receives: the run and the harness environment jigs built. #### Properties | Property | Type | | :------ | :------ | | `env` | `Record`<`string`, `string`> | | `metadata` | [`RunMetadata`](#runmetadata) | *** ### OpenedModel A live provider model and what closing it releases. #### Properties | Property | Type | | :------ | :------ | | `model` | `LanguageModel` | #### Methods ##### close() ```ts close(): Promise; ``` ###### Returns `Promise`<`void`> ## Type Aliases ### CheckResult ```ts type CheckResult = | { detail?: string; ok: true; } | { ok: false; reason: string; repair: string; }; ``` A check's outcome: a pass with an optional `detail`, or a failure with its repair. *** ### DecisionGeneration ```ts type DecisionGeneration = object; ``` What a driver's `decide` returns: one answer per question. #### Type Parameters | Type Parameter | Default type | | :------ | :------ | | `QUESTIONS` *extends* [`JevQuestions`](jigs.md#jevquestions) | [`JevQuestions`](jigs.md#jevquestions) | #### Properties | Property | Type | | :------ | :------ | | `answers` | [`JevAnswers`](jigs.md#jevanswers)<`QUESTIONS`> | *** ### DriverRequest ```ts type DriverRequest = | AgentRequest | ModelRequest | AskJevOptions | HarnessTarget; ``` Any request a driver's checks and environment allowlist are asked about. *** ### ExecutorGeneration ```ts type ExecutorGeneration = ModelGeneration & object; ``` What a driver's call returns: the reply text, provider metadata and any structured output. #### Type Declaration | Name | Type | | :------ | :------ | | `output?` | `unknown` | *** ### HarnessTarget ```ts type HarnessTarget = object; ``` A harness to open in a worktree, resuming a session when one is given. #### Properties | Property | Type | | :------ | :------ | | `cwd` | `string` | | `harness` | [`Harness`](jigs.md#harness-2) | | `resume?` | [`AgentSessionRef`](jigs.md#agentsessionref) | *** ### RunMetadata ```ts type RunMetadata = Pick; ``` The run a step belongs to: `getWorkflowMetadata()` inside the step. *** ### RunRequest ```ts type RunRequest = Extract; ``` An agent request that runs in a worktree. ## Functions ### createAgentRunner() ```ts function createAgentRunner(harness, options): Promise; ``` Open a Claude Code or Codex harness inside a factory's own step, the way the built-in agent step does: the environment allowlist with the factory's `agents.env`, the request and just-in-time checks, the worktree lock, Codex's private home and app server, and the Claude spawn hook. The returned `model` is the live provider, ready for `generateText`. #### Parameters | Parameter | Type | | :------ | :------ | | `harness` | [`Harness`](jigs.md#harness-2) | | `options` | [`AgentRunnerOptions`](#agentrunneroptions) | #### Returns `Promise`<[`AgentRunner`](#agentrunner)> #### Remarks Call it inside a `"use step"` function, never in a workflow. The step can hand the provider a function, such as a tool-approval hook or a logger, because a step runs where functions are allowed; pass it through the AI SDK call. It throws `JitCheckError` when a just-in-time check fails, and [AgentSessionError](#agentsessionerror) when `resume` names a session this harness cannot resume. Pi has no provider model, so a Pi descriptor throws: run Pi with `runAgent`. #### Example ```ts import type { AgentSessionRef, Harness } from "@jigs-ai/jigs"; import { createAgentRunner } from "@jigs-ai/jigs/steps"; import { generateText } from "ai"; import { getWorkflowMetadata } from "workflow"; export async function runWithTemperature(request: { harness: Harness; cwd: string; prompt: string; resume?: AgentSessionRef | undefined; }) { "use step"; const runner = await createAgentRunner(request.harness, { cwd: request.cwd, run: getWorkflowMetadata(), resume: request.resume, }); try { const result = await generateText({ model: runner.model, prompt: request.prompt, temperature: 0 }); return { text: result.text, session: runner.sessionFrom(result) }; } finally { await runner.close(); } } ```