agentleFS
mikhailkogan17mikhailkogan17/cycler/AGENTS.md

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…

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
  1. Common:

    Credential pre-flight: Before dispatching, the poller reads the Claude credential's *expiries* from the keychain — never the token values.

    line 120
  2. Distinctive:

    Verify, audit and refute are deliberately not downgraded — they are the checks.

    line 158
  3. Distinctive:

    No cloud, no account, no tunnel, no inbound listener.

    line 202
  4. Distinctive:

    No tokens spent by the poller.

    line 203
  5. Distinctive:

    No poller-side status writes — the workflow owns those.

    line 204
  6. Distinctive:

    No Slack, GitHub or GitLab triggers.

    line 205
  7. Distinctive:

    No genericising the harness into an empty framework.

    line 206

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

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.

halfwhatwhere it runs
triggerpoller/poller.mjs — OAuth, delegate filter, routing, dispatch, commentlaunchd, every 180s, zero tokens
processworkflows/, skills/, harness/ — contract-first implement-and-gateinside 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 repo

4. 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=app OAuth, so comments come from "Claude" rather than from the user.
  • Token refresh. Linear access tokens expire in 24h (expires_in: 86399). On AUTHENTICATION_ERROR the poller refreshes once and retries, merging the response into the stored token — a refresh response may omit refresh_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 is linear-cli, which exposes --assignee and no delegate flag, so poller/lin-delegate exists 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. --print must never appear alongside --background: they conflict, claude exits 1, and it looks exactly like the agent never saw the issue.
  • dispatch.path_prepend exists because launchd hands a job /usr/bin:/bin:/usr/sbin:/sbin. The session inherits it and cannot find node, gh, claude or lin. 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. classifyAuthFailure reads a dispatched session's transcript for the CLI's synthetic Login expired / Please run /login / Could not refresh your login error. On a match the issue is un-processed, its pending entry dropped, the issue commented, and ~/.cycler/auth-hold.json written. The hold makes the pre-flight refuse until the credential in the keychain changes (a /login).
  • poll ok means healthy. A poll that refused or found dead sessions logs poll 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-feature skill. 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 PreToolUse hooks: 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.sh checks 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, and audit.sh checks 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:

  1. $CLAUDE_PROJECT_DIR/.claude/harness/gate.sh — the repo's own. Wins whenever it exists.
  2. harness/gate.default.sh — lint/build/test autodetected from package.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 --check every .mjs/.js; bash -n every 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.