cycler
AGENTS.md0 starsChanged 17 days agoMITView on GitHub
cycler turns a Linear board into the queue for Claude Code sessions on your own machine. Assign an issue to the Claude agent in Linear; a poller on your laptop notices, starts a background Claude Code session in your repo, and that session writes a contract, implements, gates, opens a PR and reports back on the issue. This file is the spec: what it contains, how the pieces fit, and what it deliberately does not do. README.md is the user-facing…
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository14 other files it gives its agents.
- cycler-delegateSkill
- cycler-doctorSkill
- cycler-startSkill
- cycler-stopSkill
- cycler-workflow-bugSkill
- cycler-workflow-featureSkill
- cycler-workflow-intakeSkill
- cycler-workflow-researchSkill
- cycler-workflow-reviewSkill
- workflow-bugSkill
- workflow-featureSkill
- workflow-intakeSkill
- workflow-researchSkill
- workflow-reviewSkill
The file
We split this AGENTS.md into the rules it gives an agent
7 rules. About 1 is common, 6 are distinctive, and 0 are very specific to this project.
≈1
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Common: line 120
Credential pre-flight: Before dispatching, the poller reads the Claude credential's *expiries* from the keychain — never the token values.
- Distinctive: line 158
Verify, audit and refute are deliberately not downgraded — they are the checks.
- Distinctive: line 202
No cloud, no account, no tunnel, no inbound listener.
- Distinctive: line 203
No tokens spent by the poller.
- Distinctive: line 204
No poller-side status writes — the workflow owns those.
- Distinctive: line 205
No Slack, GitHub or GitLab triggers.
- Distinctive: line 206
No genericising the harness into an empty framework.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
cycler — specification
cycler turns a Linear board into the queue for Claude Code sessions on your own machine. Assign an issue to the Claude agent in Linear; a poller on your laptop notices, starts a background Claude Code session in your repo, and that session writes a contract, implements, gates, opens a PR and reports back on the issue.
This file is the spec: what it contains, how the pieces fit, and what it deliberately does not do.
README.md is the user-facing version; this one is for whoever is changing cycler itself.
1. Why it exists
Linear's agent integration delivers by webhook, and a webhook needs a public URL. So the "self-hosted" options in this space either run their networking layer in someone's cloud or ask you to tunnel — ngrok, Hookdeck — into your laptop. That is a service dependency, an inbound listener, and usually a subscription.
cycler polls outbound: an OAuth app in your own workspace, a fetch every 180 seconds from a launchd
job. Nothing listens. Nothing runs in anyone's cloud. There is no account.
Target user: solo devs who want the loop without paying for a platform. The poller is plain Node and never calls a model, so every token is spent by the workflow the user configured.
2. The two halves
cycler ships both, because either alone is useless. A poller with no process behind it burns quota producing nothing; a harness with no trigger is a thing you have to remember to run.
| half | what | where it runs |
| trigger | poller/poller.mjs — OAuth, delegate filter, routing, dispatch, comment | launchd, every 180s, zero tokens |
| process | workflows/, skills/, harness/ — contract-first implement-and-gate | inside the dispatched session |
3. Layout
cycler/
README.md AGENTS.md LICENSE cycler.example.yaml
.claude-plugin/plugin.json the plugin manifest
commands/ the only user interface — no CLI
start.md stop.md issue.md doctor.md
hooks/hooks.json the four PreToolUse hooks, plugin-rooted
lib/yaml.mjs the config reader, shared by poller and hooks
poller/poller.mjs the poller
poller/lin poller/lin-delegate the Linear CLI the harness falls back to
skills/{task,research,intake}/SKILL.md
workflows/task-orchestration.js the ten-phase workflow
harness/
HARNESS.md ISSUE-PROCESS.md PIPELINE.md ROUTING.md CONTRACT.md modes/
gate.sh the gate RESOLVER — the one gate command
gate.default.sh used only when the repo has no gate of its own
audit.sh file-followups.sh link-workspace.sh worktree-gc.sh
read-config.mjs one config key, for the bash hooks
hooks/ tests/
CLAUDE.reference.md template CLAUDE.md for a consuming repo4. Interface
Slash commands only — no CLI, no npm binary, no second install surface. /cycler:start,
/cycler:stop, /cycler:delegate <KEY>, /cycler:doctor. They are command files that instruct the
session to run the underlying bash. The skills in skills/ land in the same /cycler: namespace but
are not commands: they are the workflows a dispatched session runs, and they carry a workflow-
prefix so a list showing both halves says which is which. cycler:workflow:feature would say it
better and is not available — a skill name may hold only letters, digits, underscores and hyphens,
so the one colon is the plugin's. /cycler:start is idempotent by design: it checks what is
already configured and does only the missing parts, so "set it up" and "switch the loop back on"
are the same command. Two commands for those would mean a user who ran the wrong one gets a
poller that is configured and not running.
5. Config
One file: ~/.config/cycler/config.yaml (honours $XDG_CONFIG_HOME; $CYCLER_CONFIG overrides).
Credentials, repo, workflows and dispatch all live there. It used to be two — a committed
cycler.yaml in the repo plus ~/.cycler/config.json for the OAuth credentials — and every question
about cycler began with "which file?". Splitting a config by secrecy split it by nothing else:
repo.base and client_secret are both typed once and never thought about again. Outside the repo,
so nothing is one git add . from a public history.
~/.cycler/ survives but holds no config — only state the poller writes: token.json,
processed.json, the launchd logs. A file a program rewrites every 24h cannot also be the file you
hand-edit.
Keys are canonically snake_case, and pick() resolves camelCase and kebab-case onto the same key. A
key spelled the old way must not read as absent: dispatch.pathPrepend silently unread means a
dispatched session with launchd's bare PATH, stalling on a missing node.
lib/yaml.mjs parses the subset actually used: nested maps, scalars, inline lists, block lists, and
block lists of maps. It never throws — a malformed config degrades to defaults rather than stopping a
poll, because a poller that dies on config stops dispatching silently.
Environment overrides exist for the values launchd needs to force without editing a file:
REPO_PATH, CLAUDE_BIN, CYCLER_WORKFLOW, CYCLER_HOME, CYCLER_CONFIG.
6. The poller
actor=appOAuth, so comments come from "Claude" rather than from the user.- Token refresh. Linear access tokens expire in 24h (
expires_in: 86399). OnAUTHENTICATION_ERRORthe poller refreshes once and retries, merging the response into the stored token — a refresh response may omitrefresh_token, and dropping it makes the next refresh impossible, turning a self-healing poller into one that dies a day later. - Selected by
delegate.issues(filter: { delegate: { id: { eq: viewer.id } } })— that is the field an agent lands in, and assigning one in Linear's UI is what puts it there. It is not a distinction a user has to know about; the one place it leaks islinear-cli, which exposes--assigneeand no delegate flag, sopoller/lin-delegateexists for CLI-driven starts. - Routing is a lookup on a label a human already wrote —
research→/workflow-research, everything else →workflows.default. Deliberately not a classifier: a model would infer, less reliably, something already recorded, and a router that returns the default for everything is indistinguishable from a working one until something audits its choices. - Dispatch is a configurable template (
dispatch.command), split like a shell would but without a shell — issue titles contain quotes, backticks and$. Placeholders are substituted after splitting, so a title can never introduce an argument.--printmust never appear alongside--background: they conflict,claudeexits 1, and it looks exactly like the agent never saw the issue. dispatch.path_prependexists because launchd hands a job/usr/bin:/bin:/usr/sbin:/sbin. The session inherits it and cannot findnode,gh,claudeorlin. An interactive session never sees this, which is why it only appears once dispatch is automated.- Credential pre-flight. Before dispatching, the poller reads the Claude credential's expiries from the keychain — never the token values. It refuses to dispatch if the access expiry is missing or unreadable (the 1970-01-01 case), or if the access token is stale and its refresh token cannot renew it. A stale access token with a live refresh token lets one issue out, so exactly one process refreshes. A refusal logs, notifies once, and leaves the issue unprocessed with no attempt spent.
- Logged-out sessions are terminal, not retried.
classifyAuthFailurereads a dispatched session's transcript for the CLI's syntheticLogin expired/Please run /login/Could not refresh your loginerror. On a match the issue is un-processed, its pending entry dropped, the issue commented, and~/.cycler/auth-hold.jsonwritten. The hold makes the pre-flight refuse until the credential in the keychain changes (a/login). poll okmeans healthy. A poll that refused or found dead sessions logspoll degraded: …instead — one greppable line.- Every dispatch and every failure posts a comment. Without the failure comment, a failed dispatch is indistinguishable from an issue the agent never saw.
- Idempotent via
processed.json; completed and canceled issues are skipped; a failed dispatch is not marked processed, so it retries on the next poll. - Not the poller's job: issue state. Transitions belong to the workflow, which knows the
outcome —
linearSync('started' | 'open-questions' | 'pr-opened' | 'approved' | 'blocked-<stage>'), performed by the/workflow-featureskill. A poller watching process liveness only knows a process ended.
7. The harness
Ten phases: Contract → Branch → Implement → Audit → Verify → Commit → PR → Review → Follow-ups →
Cleanup.
- Contract first, with
[user]/[paraphrase]/[inferred]provenance on requirement lines. - Enforcement outside the agent — four
PreToolUsehooks:require-contract.sh(no edits before a contract),require-green-gate.sh(no commit on a red gate),require-escape-hatch.sh(a large change runs the full workflow instead of inline),confine-to-worktree.sh(no writes outside the session's worktree). - Audit is arithmetic before judgement.
audit.shchecks paths, scope, secrets and whether the run edited its own contract; then exactly one agent answers the question a script cannot. - Four review lenses in parallel; refuters only for blocking findings; only the lens that raised one is re-run after a fix.
- Follow-ups become issues — verified only, losable only, at most 3 per run, filed via
poller/lin, because an MCP server does not reliably connect in a dispatched session. - Model tiering. Cheap models for branch, commit, PR, cleanup, follow-ups and the test-gaps lens. Verify, audit and refute are deliberately not downgraded — they are the checks.
.claude/**is not a forbidden path. A run may work on the harness and still pass its own audit. What stays forbidden is a run editing the contract that governs it or the gate that judges it — that is moving the goalposts, andaudit.shchecks for it.
The rule underneath all of it: green is only evidence if the check could have gone red. Six
checks in this harness's history were found incapable of failing on the input they judged — a
predicate returning literal true, a count/slice pairing no test could reach, a cross-language check
matching its own doc comment, a Swift gate that skipped Swift, a contract hook satisfied by renaming
a file, routing that dispatched the same workflow for everything, and a config-driven test whose
config was never loaded — which hid three separate ReferenceErrors through a fully green suite.
Each looked green for weeks.
8. The gate is repo-local
harness/gate.sh is a resolver, and it is the one gate command:
$CLAUDE_PROJECT_DIR/.claude/harness/gate.sh— the repo's own. Wins whenever it exists.harness/gate.default.sh— lint/build/test autodetected frompackage.json.
It execs, so the chosen gate's output and exit status pass through unchanged and every caller sees
the same output contract. It prints which gate it chose to stderr, never stdout: stdout is the
output contract, and a resolver that quietly always picked the default would look identical to a
working one.
The default reports FAIL, not a vacuous pass, when it finds no script to run.
9. Verification
node --checkevery.mjs/.js;bash -nevery shell script.node harness/tests/run.mjs— the harness's own suite ships with it.- The resolver must be able to go red: it must pick the repo's gate in a repo that has one and the default in a repo that does not.
Not yet done: an end-to-end run in a repo that is not the one this grew in — a live delegation through a real session to a real PR. The deterministic halves are verified in a scratch repo (gate resolution both ways, the default gate going green and red on real outcomes, the commit hook accepting a gated tree and refusing an edited one), but the full loop has only ever completed in applygent. It is the only test of the headline claim and it belongs here, under gaps, until it happens.
10. Non-goals
- No cloud, no account, no tunnel, no inbound listener.
- No tokens spent by the poller; routing stays a label lookup.
- No poller-side status writes — the workflow owns those.
- No Slack, GitHub or GitLab triggers.
- No genericising the harness into an empty framework. It ships opinionated and working.
11. Portable runtimes and GitHub review
The Claude-specific descriptions above document the legacy default. dispatch.runtime now also
supports codex and dsh: poller/managed.mjs owns detached workers, workflows/run.mjs supplies
the runtime primitives for the same lifecycle, and lib/runtimes.mjs owns executable/model routing.
Managed runs never depend on the Claude keychain or registry. Audit and gate run deterministically
before commit stages. Claude PreToolUse hooks remain Claude-specific; do not claim they protect
other runtimes. Native skills enable githubReview: true; portable runs enable it by default.
harness/review.mjs is separate from implementation and Linear. The workflow-review skill runs it
standalone. It fetches the exact PR head into a detached worktree, runs independent reviewers,
verifies blockers and publishes actual GitHub COMMENT reviews with validated inline locations.
Missing results and stale heads fail. Implementer replies go to the original threads with a pushed
commit SHA. .agents/skills wrappers expose all workflows and commands to Codex and dsh.
Full setup and current live-test limits: docs/specs/005-runtimes.md.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

