Sno Station
Sno Station is your agents' workstation: open-source software that turns the AI agents you already run into one squad on your own machine. Two heads are better than one, and they get better with every
Shared AI agent workflow infrastructure for reusable local and remote development loops
PluginsJavaScript · TypeScriptGitHub @mfittkoSelf-installOwner unconfirmed
Paste into Claude or ChatGPT for installation instructions suited to your environment.
Have you tried it? One tap is enough. No sign-in needed.
Loading the full README. The saved excerpt is shown below.
Turn a GitHub issue into a merged PR autonomously, up to a human-approval checkpoint.
Harness-agnostic by design. The same dev loop runs three ways over one shared core (packages/core): as a Claude Code plugin, as a Pi extension, and as a standalone CLI. Routing, gates, and phases are defined once in the core; the plugin and extension are thin integrations over that shared workflow.Read the full README ↗dev-loops ships reviewed, tested PRs with the highest autonomy that is still safe. Deterministic gates and a judge decide; humans decide only what tooling cannot, or what the repo requires.
Turn a GitHub issue into a merged PR autonomously, up to a human-approval checkpoint.
Harness-agnostic by design. The same dev loop runs three ways over one shared core (packages/core): as a Claude Code plugin, as a Pi extension, and as a standalone CLI. Routing, gates, and phases are defined once in the core; the plugin and extension are thin integrations over that shared workflow.
A dev loop takes a GitHub issue through seven lifecycle phases — from intake to merge — with deterministic routing, self-correcting review gates, and autonomous execution until a human approves. The phase is always consultable from the deterministic state model in packages/core/src/loop/lifecycle-state.mjs.
| Phase | What happens |
|---|---|
issue_intake | Normalize the issue, confirm scope, detect linked PRs |
refinement | Elaborate the spec, run a bounded audit, harden acceptance criteria |
implementation | Build the accepted scope on a feature branch or via Copilot |
draft_gate | Gate review at the draft→ready boundary before marking the PR ready |
feedback_resolution | Fix, reply to, and resolve review threads on GitHub |
pre_approval_gate | Final gate review: verify evidence, CI, and unresolved threads |
merge | Merge the PR and write the retrospective checkpoint |
The public routing contract is the Public Dev Loop Contract.
dev-loop is the single public entrypoint. Drive it in natural language — it resolves the authoritative current state, picks the correct internal strategy, and routes deterministically. You never name internal strategies.
start dev loop on issue 112 # start work on an issue
auto dev loop on issue 112 # run autonomously to the human-approval checkpoint
continue dev loop on PR 88 # continue follow-up on an open PR
See the canonical shorthand mapping in the Public Dev Loop Contract.
dev-loop (natural language) is the catch-all router — reach for the named commands below only when you want a direct, unrouted entrypoint. The syntax differs by harness:
/loop-start, /loop-auto, …/dev-loops start, /dev-loops auto, …| Command | Claude Code | Pi | Does |
|---|---|---|---|
| start | /loop-start <issue> | /dev-loops start <issue> | Start a dev loop on an issue |
| auto | /loop-auto <issue> | /dev-loops auto <issue> | Run autonomously to the human-approval checkpoint |
| continue | /loop-continue [issue|pr] | /dev-loops continue [issue|pr] | Continue; bare resumes the in-progress board item |
| start-spike | /loop-start-spike <question> | /dev-loops start-spike <question> | Time-boxed spike (or --file <path>) |
| info | /loop-info <issue|pr> | /dev-loops info <issue|pr> | Read-only state summary |
| status | /loop-status | /dev-loops status | Readiness check (gh auth, git repo, subagent) |
| enqueue | /loop-enqueue <issue|pr|text> | — | Queue an issue/PR, or capture an idea as a grilled issue |
| grill | /loop-grill <issue|plan> [--auto] | — | Socratic Q&A grill of an issue or plan before the loop |
| queue-status | /loop-queue-status | — | Show the queue board grouped by column |
| review | /loop-review <pr> | — | Run a standalone, on-demand review pass over a PR (no gate obligations) |
| review-ui | /loop-review-ui <pr> | — | Review a PR in a running-app UI loop |
Beyond the per-issue loop entrypoints, the Pi /dev-loops command and the dev-loops CLI also expose standalone utilities: help, status, doctor, gates (status is the same readiness check as loop-status above). hide works only inside Pi — the dev-loops hide CLI subcommand is recognized but intentionally exits non-zero (session-local Pi UI behavior). Everything above is also available as a skill/agent — inside Pi, hand work to the dev-loop skill rather than calling internal routed skills (local-implementation, copilot-pr-followup, final-approval) directly.
/loop-review-ui/loop-review-ui <pr> reviews a PR by proving the change in the running app from an isolated worktree, rather than reading the diff alone. It runs the loop resolve → provision/boot → auth+drive → diagnose → report → teardown: it provisions a worktree for the PR head and boots the app, authenticates and drives the changed UI flows (capturing a screenshot + state + snapshot per step), maps captured failures back to the diff, posts a head-pinned pending PR review plus a self-contained screenshot artifact, then tears down — always emitting a side-effect ledger.
When to use it vs. the standard review angle: reach for /loop-review-ui when a change is only really provable by exercising it in a browser — a rendered flow, a create/edit/upload interaction, or a swallowed server error the UI hides behind a success state. It is the running-app sibling of, not a replacement for, the diff-reading review angle or the Copilot gate; those still run for logic, structure, and correctness. It requires a per-project run/auth recipe in .devloops and runs end-to-end on any harness; only the report-artifact hosting is harness-aware. On Claude Code the self-contained artifact gets a zero-setup hosted link (Claude Artifacts); off-Claude the same artifact is still produced but emitted unhosted with a stated reason (a hosted GitHub-native link is a tracked follow-up), never a hard stop. See the UI-Review Run/Auth Recipe Contract to wire a consuming repo.
All examples pin to <version> = the dev-loops version you installed; keep the CLI, plugin, and extension on the same version so behavior can't drift.
The repo ships a plugin rooted at .claude/ (manifest at .claude/.claude-plugin/plugin.json) exposing the dev-loop agents, skills, and hooks. Install it from the bundled marketplace catalog:
/plugin marketplace add mfittko/dev-loops # register the marketplace
/plugin install dev-loops@dev-loops # install the plugin
Or load it directly for a single session without installing:
claude --plugin-dir .claude
Installed from npm, point at the bundled copy: claude --plugin-dir node_modules/dev-loops/.claude.
The hooks provide the gh pr ready draft-gate guard, an opt-in read-only boundary for the main agent (DEVLOOPS_MAIN_AGENT_READONLY=1), and an opt-in coordinator→worker delegation boundary — a dispatched dev-loop coordinator delegates tracked-repo-file mutations and targeted code-verification/build checks to a worker subagent rather than running them inline (DEVLOOPS_COORDINATOR_READONLY=1). The hook blocks known inline verification commands; local full-repository validation (bun run verify) is requested only through dev-loops gate resolve-validation on a clean commit, not delegated as a direct worker command. Skill references to a consumer repo's own PLAN.md / AGENTS.md resolve against that repo, by design.
The dev-loop coordinator agent sets experimental.cacheTtl: 1h in its frontmatter. The coordinator often waits more than 5 minutes for its child agents, and the 1-hour prompt cache keeps its context warm across those waits. No other agent sets the key, so short-lived roles keep the cheaper 5-minute default. The key needs Claude Code v2.1.248 or later. Claude Code ignores the key for subagents while a subscription draws on usage credits.
Auto-mode permission rules. The gate verdict post is the sanctioned gate path. The Claude Code auto-mode classifier can still deny it (observed reason "[CI Bypass]"), and it can deny the ready flip ready-for-review.mjs (observed reason "[Auto-Mode Bypass]"). Add an allow rule to permissions.allow in the project .claude/settings.json for each form the agents use:
Bash(dev-loops gate upsert-verdict:*) for the CLI verdict post;Bash(node scripts/github/upsert-checkpoint-verdict.mjs:*) for the bare script verdict post, with the script path exactly as the agent types it;Bash(node scripts/github/ready-for-review.mjs:*) for the bare script ready flip;Bash(dev-loops-run:*) for the launcher form dev-loops-run scripts/.... This repo's own .claude/settings.json carries it.Observed during the v1.0.5 drain: only an exact command prefix matched, and a mid-pattern wildcard such as Bash(node scripts/github/*.mjs:*) did not match. In the same drain, an ask rule on the bare form did not gate the launcher form: with Bash(dev-loops-run:*) allowed, Bash(node scripts/github/merge-pr.mjs:*) did not prompt for dev-loops-run scripts/github/merge-pr.mjs. For a merge or filing prompt, add the launcher-form ask rules Bash(dev-loops-run scripts/github/merge-pr.mjs:*) and Bash(dev-loops-run scripts/github/create-issue.mjs:*).
Gate reviewers write their findings artifacts under tmp/gate-reviews/ with the Write tool. The Write tool path needs no allow rule. The classifier denies a Bash write of an artifact (redirect, heredoc, tee, or an inline script), observed with reasons "[Self-Approval]" and "[CI Bypass]". A Write(...) or Edit(...) path rule does not apply to a Bash command. A reviewer's blocked artifact goes through dev-loops-run scripts/github/emit-reviewer-blocked.mjs under the Bash(dev-loops-run:*) rule above.
Consumer entry contract. The plugin ships prompts, skills, agents, and hooks, plus a hand-authored .claude/package.json + .claude/package-lock.json that pin the published dev-loops npm package to the release version. Claude Code's native plugin dependency auto-install runs npm ci --ignore-scripts at cache time, landing the full toolchain under .claude/node_modules/dev-loops/ (only the manifest and lockfile are committed — never node_modules). Every generated skill/command/agent invocation runs through the dev-loops-run launcher on the plugin bin/ PATH, which resolves the dev-loops root in order: a live source checkout found by walking up from the current directory always wins unconditionally (so local edits are dogfooded with no release), else a live source checkout found by walking up from the launcher's own real binary location, else the auto-installed .claude/node_modules/dev-loops, else a loud hard-stop that names the unresolved wrapper — never a raw node scripts/*.mjs against the checkout, and never a silent raw-gh fallback. The single-source CLI subcommand reference is tracked in issue #1376.
pi install npm:dev-loops@<version> # global, pinned npm package
pi install -l npm:dev-loops@<version> # project-local (.pi/settings.json)
You can also install from git:
pi install git:github.com/mfittko/dev-loops@<tag-or-sha> # global
pi install -l git:github.com/mfittko/dev-loops@<tag-or-sha> # project-local
Once a project is trusted, Pi auto-installs missing packages on startup. Install pi-subagents the same way when the repo relies on async loop behavior.
Run the CLI version-pinned:
npx dev-loops@<version> --help
A global npm install -g dev-loops is an optional bare-shell convenience only — it is not version-pinned, updates independently of the plugin/extension, and can silently drift out of sync (#833/#1036). Prefer the pinned npx invocation.
Universal:
>=24gh installed and authenticated for GitHub/Copilot workflowsFor contributors and CI:
1.4.1 exactly — the repository's package manager, workspace installer, script runner, and unit-test runnerbun install --frozen-lockfile from a clean checkout; bun.lock is the only supported dependency lockfileThis split is deliberate: published packages and the public CLI continue to install and execute with Node >=24 and do not require Bun. npm remains the registry client for packing, publishing, dist-tags, registry queries, and provenance.
Pi-harness only:
pi-subagents for async workflow behavior@earendil-works/pi-coding-agent and @earendil-works/pi-tuiDefaults ship in packages/core/src/config/extension-defaults.yaml. A repo-local .pi/dev-loop/defaults.* layer merges on top of them when present, then consumer repos override everything in a .devloops file at the repo root (.devloops.yaml / .yml / .json are also accepted); .devloops is authoritative when present. The legacy .pi/dev-loop/settings.* and overrides.* fallbacks were removed at the v1.0.0 cut (ADR 0017) and no longer load; move any such file's contents into .devloops.
npx dev-loops@<version> gates # print what reviewers will check
Key surfaces:
requireCi CI-precondition toggle (gates.draft.requireCi, gates.preApproval.requireCi; default true). Set a gate's requireCi: false to opt that gate out of the CI precondition — e.g. a repo with no CI can run the loop end-to-end instead of blocking at the gate.refinement.maxCopilotRounds caps Copilot re-review rounds (default 5). Set maxCopilotRounds: 0 to disable the Copilot gate entirely — local-harness-only review (draft_gate → pre_approval_gate), useful when the repo has no Copilot reviewer configured. By default one converged Copilot review stands for later heads, and pre_approval_gate reviews each head; set refinement.requireCopilotConvergenceAtLatestHead: true to open a new Copilot cycle for every significant post-convergence change.uiReview surface configures the /loop-review-ui route (per-project run/boot command, login recipe, log/exception patterns)postMerge.actions declares local actions (sync checkout, restart a local service, smoke check) a harness hook runs sequentially, in order, after the dev-loop's merge succeeds. Each action needs name + run; optional onlyIfChanged scopes it to the merged PR's changed files (plain substring matching, not a glob/regex — a pattern matches when any changed file path contains it), verify polls a command until it exits 0, and timeoutMs/verifyTimeoutMs/verifyIntervalMs bound the run/verify/poll timing. Absent (the default) means no action runs. See packages/core/src/loop/run-post-merge-actions.mjs for the runner and scripts/loop/run-post-merge-actions.mjs for the CLI both harness hooks invoke.Full details: the shipped defaults in packages/core/src/config/extension-defaults.yaml and the loader in packages/core/src/config/config.mjs.
From 1.0, dev-loops follows semantic versioning for its public surface — breaking changes to it bump the major version:
.devloops configuration surface — gate angles, personas, gates.* (including each gate's requireCi toggle and gates.preApproval.requireCi), refinement.*, uiReview.*, postMerge.*, autonomy.*, and workflow.*. The authoritative validator is the config loader in packages/core/src/config/config.mjs; schemas/dev-loop-config.schema.json is generated from it (node scripts/generate-config-schema.mjs) and covers the full file-level surface — only cross-field refinements stay zod-only.dev-loop command and skill surface — the natural-language dev-loop router and the named /loop-* entrypoints.Internal strategy names, script internals, and undocumented helpers are not part of the stable surface and may change in a minor release. New config keys and gate angles are additive (minor); removing or repurposing an existing one is a breaking change (major).
A deterministic container image with all required tooling for dev-loop operation.
docker build -t dev-loops .
| Variable | Purpose | Required |
|---|---|---|
GH_TOKEN | GitHub token for gh CLI and API calls | Yes |
OPENAI_API_KEY | LLM provider key (only for pi / LLM-backed operations) | No |
Smoke test the image with a minimal read-only info call:
docker run --rm -e GH_TOKEN="$GH_TOKEN" dev-loops dev-loops loop info --repo mfittko/dev-loops --issue 1
Verify the toolchain is reachable:
docker run --rm dev-loops node --version
docker run --rm dev-loops pi --version
docker run --rm dev-loops dev-loops --version
docker run --rm dev-loops gh --version
The Dockerfile pins exact versions for Node.js (base image), Bun 1.4.1, the pi CLI, pi extensions, and gh CLI; paired with the committed bun.lock and a frozen Bun install, repeat builds produce functionally identical toolchains. Node remains present because it is the shipped consumer runtime and runs the Playwright browser suite.
Runtime patterns:
# Interactive Pi sharing host config (sessions, models, settings write back to ~/.pi)
docker run -it --rm -e GH_TOKEN="$GH_TOKEN" -v "$HOME/.pi:/home/node/.pi" dev-loops pi
# Interactive Pi, clean (ephemeral ~/.pi; provider auth via env)
docker run -it --rm -e GH_TOKEN="$GH_TOKEN" -e OPENAI_API_KEY="$OPENAI_API_KEY" dev-loops pi
# Full dev-loop over a live repo worktree mounted at /workspace
git clone --mirror [email protected]:owner/repo.git /tmp/mirror
git --git-dir=/tmp/mirror worktree add /tmp/run /tmp/mirror/main
docker run -it --rm -e GH_TOKEN="$GH_TOKEN" -v "$HOME/.pi:/home/node/.pi" -v /tmp/run:/workspace dev-loops pi
bun install --frozen-lockfile
bun run test:docs # example targeted check for documentation changes
Choose targeted checks with resolveTargetedValidation(changedPaths) as described in the validation policy. For an authorized full local PR-gate run, use node cli/index.mjs gate resolve-validation --profile full-repository --repo <owner/name> --pr <number> --gate <gate> --head-sha <full SHA> from the PR worktree.
CI installs the exactly pinned Bun 1.4.1 and uses bun install --frozen-lockfile. It splits the complete Bun test inventory into four parallel, timing-balanced shards, runs docs and workflow validators separately, and joins them through a fail-closed verify gate. The conditional Playwright/WebKit viewer smoke remains a Node-run browser test and runs only when its bounded viewer surface or smoke-path dependencies change.
Upgrading from before the rename to dev-loops? The package name, repo slug, and all PI_* environment variables changed (the env vars are a clean break — no aliases). See the migration guide.
uiReviewMode: vision) review loop/loop-review-ui