sykli — Finnish for cycle
Run your checks. Reuse the work. Explain the result.
Demo · Quick start · How it works · CI · Agents · Commands
Sykli is one binary that turns your repository's checks into a declared graph. It runs the graph in order, reuses any result whose inputs haven't changed, and writes a receipt that ties every outcome to the exact tree it ran against. It works the same on your laptop, in CI and for a coding agent.
Keep Cargo, npm, Go and your shell scripts. Sykli doesn't replace your tools. It decides which of them need to run and keeps a verifiable record of what did.
Two tasks, lint then test, each reading one script. Real output, hashes shortened:
$ sykli run
running: lint
lint passed
passed: lint
running: test
tests passed
passed: test
receipt: …/.sykli/receipts/rcpt_4a80a5dc….json
$ sykli run # nothing changed: nothing runs
cached: lint
cached: test
receipt: …/.sykli/receipts/rcpt_efdf9834….json
$ echo 'echo tests passed, again' > test.sh
$ sykli plan --explain # what would a run cost now?
lint
selected: all tasks requested
cache: available; receipt rcpt_4a80a5dc….json (restoration not attempted)
test
selected: all tasks requested
cache: no entry for the current key
Input coverage against 435c318a… (plus --changed hints):
declared input: "test.sh" -> test
Unmapped paths are advisory except for missing Cargo inputs, which block execution and cache reuse.
$ sykli run --json > .sykli/receipt.json # only the changed task runs
cached: lint
running: test
tests passed, again
passed: test
$ sykli verify .sykli/receipt.json --contract sykli.json
ok: schema sykli-receipt.v1
ok: contract 8d0e65a0…
ok: tree 777d2ada…
ok: inputs e533f926…
ok: records consistent
ok: outcome passed with 2 task records
verified: receipt matches this tree and contract
$ echo 'echo sneaky' > lint.sh # change the code after the fact…
$ sykli verify .sykli/receipt.json --contract sykli.json
ok: schema sykli-receipt.v1
ok: contract 8d0e65a0…
mismatch: tree expected a07b2a09… but got 777d2ada…
mismatch: inputs expected 834ffc9b… but got e533f926…
$ echo $?
3Once the code changes, the old receipt no longer verifies. The whole graph is below; copy it and try this yourself.
Install from source with Rust 1.85 or newer:
cargo install --git https://github.com/false-systems/sykli --branch main --locked sykliThen, in an existing Cargo, npm or Go repository:
sykli init # detect ecosystems, write sykli.json, pin it in sykli.lock
sykli plan --explain # see selection and cache evidence, run nothing
sykli run --json > .sykli/receipt.json # run the graph, keep the receipt
sykli verify .sykli/receipt.json --contract sykli.jsoninit creates .sykli/ for local state and adds it to .gitignore. Review the
inputs it declares, especially configuration and fixtures, and declare files you
add later. Task execution needs Git, a POSIX sh and your project's own
toolchain. On Windows, Git for Windows supplies sh.
Note
This README describes main. For a reproducible install, replace
--branch main with --rev COMMIT. Tagged downloads will be listed on the
releases page once
published, and install.sh verifies their checksums. Installing
Sykli does not install your build tools.
flowchart LR
contract["<b>sykli.json</b><br/>tasks · inputs · after"]
tree["<b>Git tree</b><br/>declared input files"]
cache[(".sykli/cache")]
subgraph run["sykli run · each task, in dependency order"]
key["Cache key<br/>declaration + input digests<br/>+ upstream keys"]
hit{"Cached pass<br/>validates?"}
reuse["Reuse"]
exec["Execute"]
key --> hit
hit -- yes --> reuse
hit -- no --> exec
end
receipt["<b>Receipt</b><br/>tree · inputs · contract"]
verify{{"<b>sykli verify</b><br/>receipt vs. the tree now"}}
plan["<b>sykli plan --explain</b><br/>same question,<br/>runs nothing"]
contract --> key
tree --> key
reuse --> receipt
exec --> receipt
receipt --> verify
exec -- pass --> cache
cache -.-> hit
plan -.-> hit
It comes down to three things:
| What it is | Where it lives | |
|---|---|---|
| Contract | The work you declare: tasks, commands, the files each one reads, and what runs after what. |
sykli.json, pinned by sykli.lock |
| Receipt | One evaluation: commands, outcomes, output digests, and which results were reused. | .sykli/receipts/ (and wherever you redirect --json) |
| Verify | A check that a receipt is consistent with the tree and contract in front of you. | sykli verify |
Reuse is earned, not assumed. A task's cache key covers its declaration,
the contents and execute bits of its declared inputs, the runtime fingerprint,
digests of inherited environment values, and the cache keys of every task it
runs after. A downstream task never reuses a pass recorded against upstream
outputs it did not see, and a matching entry must validate before it is used.
In a Cargo graph the runtime fingerprint also covers the Rust toolchain the
task shell resolves (rustc -vV, cargo -V), so a pass recorded under one
compiler is not reused under another.
In a scratch Git repository:
printf 'echo lint passed\n' > lint.sh
printf 'echo tests passed\n' > test.sh{
"schema": "sykli-contract.v1",
"tasks": [
{ "name": "lint", "run": "sh lint.sh", "inputs": ["lint.sh"] },
{ "name": "test", "run": "sh test.sh", "inputs": ["test.sh"], "after": ["lint"] }
]
}inputs names the files a task reads. after makes test wait for lint and
binds its cache key to that dependency. Independent tasks run in parallel.
Every path a task declares (inputs, outputs, workdir) is relative to the
repository and stays inside it. Absolute paths and .. are refused when the
contract is validated, before anything runs, so a receipt is always a claim
about the repository it sits in. sykli validate checks a contract without
running it.
What a task sees when it runs
Tasks run under sh -c with no stdin. They receive PATH, HOME and TMPDIR,
plus declared env values and named inherit variables. Inherited values reach
the task, but only their digests reach the receipt, where they count toward the
task's identity. Task output is captured, so commands are responsible for what
they print. Sykli runs local commands with your permissions.
plan --explain tells you what a run would do and what the cache can supply,
without running anything:
sykli plan --explain
sykli plan --changed test.sh --explain
sykli plan --changed test.sh --explain --json
sykli plan --explain --base origin/main --json
sykli plan --explain --preview --json # inspect a contract edit before re-lockingIt reads sykli.json by default; an explicit JSON contract path selects another
graph. It runs no task or Rust emitter, restores no outputs, and writes no cache
entries or receipts.
| Cache state | What it tells you |
|---|---|
available |
Cached evidence and artifacts validate; a run can attempt reuse. |
missing |
No entry exists for the current content key. |
invalid |
An entry exists, but its evidence or artifacts failed validation. |
deferred |
A dependency needs execution, restoration or error resolution first. |
input_error |
A declared input cannot currently be evaluated. |
Undeclared files are flagged. Explain warns about changed files that no task
lists as an input, even when every task is already selected. It checks staged,
unstaged and untracked non-ignored files, and --base origin/main adds
committed changes relative to that ref. Contract and lock changes are labeled as
evaluation metadata. In Cargo-root repositories, run refuses to execute or
reuse passes when known source, configuration or test files in the current Git
inventory are missing from every task's inputs. Other unmapped paths are advisory.
Important
Sykli does not observe every file or tool your commands read. An undeclared dependency can make a cached result misleading, and selecting every task does not prove every dependency is declared. An explanation describes the current local evidence, not a guaranteed outcome. See the coverage limits.
--changed takes paths you supply and filters the displayed plan to
matching inputs and their downstream tasks. It does not discover a Git diff and
does not restrict a later run, which evaluates the whole graph and decides
reuse from content keys.
After checkout, install Sykli and your project's toolchain, then:
mkdir -p .sykli
sykli run --json > .sykli/receipt.json
sykli verify .sykli/receipt.json --contract sykli.jsonFail the job on either command's nonzero exit and keep the receipt as an
artifact. The included GitHub Action runs and verifies the graph,
writes a job summary, uploads the receipt, and reports which tasks a pull
request affects. It installs a release tag, so pin it with
uses: false-systems/sykli@vX.Y.Z once one is published. This repository gates
itself through the Action on every pull request.
Measuring smarter CI: the shadow experiment
On pull requests, this repository runs a shadow experiment. It records the changed paths and runs the full candidate graph, then compares that result with the reuse proposed by a separate run of the base commit. It reports agreements, disagreements, unmapped paths and the task time that could have been reused.
All required checks still run. The experiment measures whether skipping could be justified later. It does not claim CI savings today, and matching observations do not prove that declarations are complete.
The CLI does the querying, and --json gives scripts and agents the same
answers. Every machine-readable document has a versioned schema, and
incompatible changes get a new version. Exit codes are part of each command's
contract and are printed in its --help. The loop an agent runs is the one you
run:
sykli plan --changed PATH --explain --json # which tasks does this change need?
sykli run --json # run them, get a sykli-receipt.v1
sykli verify RECEIPT --contract sykli.json # does that receipt still describe this tree?Read the agent guide for selection reasons, JSON fields and error behavior.
On Linux and macOS, typed production captures declared source, builds an artifact and runs the required checks. A production ID lets another terminal or agent resume from the same local store.
sykli init --production --smoke '"$SYKLI_INPUT_executable" --help'
sykli targets
sykli produce TARGET --stop-after build
sykli resume PRODUCTION_IDUse the target name and production ID that Sykli prints. Stopping after build exits 1 while checks remain. Artifact availability and completed checks are recorded separately. Failed work requires an explicit retry, and an unresolved lost attempt cannot silently become a success. See the production agent loop for preparation without execution, bounded parallel work and diagnostics.
With an authenticated GitHub CLI (gh), collect CI and review evidence for an
exact candidate and assess it against requirements you declare:
sykli inspect --repo OWNER/REPO --pr NUMBER --requirements review.json
sykli assess BUNDLE --requirements review.jsonThe requirements file names exact workflow and reviewer IDs. Sykli saves the collected responses, so an assessment can be replayed offline. Each requirement comes back established, refuted or unproven, with the gaps and excluded evidence behind it. An approval of an older commit does not count as approval of the current candidate.
Assessments are advisory. They establish declared review-readiness conditions,
not permission to merge or deploy. inspect makes bounded, read-only requests
through gh. It never posts reviews or triggers work.
verify runs in stages, and the first stage that fails decides the exit code:
| Exit | Meaning |
|---|---|
0 |
Receipt matches, and the work passed or was reused. |
1 |
The recorded work failed. |
2 |
The receipt cannot be verified. |
3 |
The tree or declared inputs changed since the run. |
4 |
The contract or lock differs. |
A receipt checks record consistency, contract identity, the repository tree and declared inputs, and then the recorded outcome. It does not authenticate who wrote the receipt, and it does not prove that the declared checks are sufficient.
A receipt also records when. started_at_ms on the run is when that run
began; on a task, it is when that task was seen running. A cached task replays
its original moment, so a receipt full of cache hits reports old task times
under a new run time, which is an accurate record of what happened. Neither
field is part of any cache key.
Keep receipts under .sykli/ or outside the repository, so that they don't
change the tree they describe.
The cache, the receipts and production scratch are bounded, and each limit is stated:
| State | Limit | Override |
|---|---|---|
| Task cache | 1 GiB; least recently used entries are dropped when a run would go over | SYKLI_CACHE_BUDGET_BYTES (0 keeps everything) |
| Receipts | 200; oldest removed first, never one that a live cache entry cites | SYKLI_RECEIPT_BUDGET (0 keeps every receipt) |
| Production attempts | Working directories removed (best effort) once outputs are collected | — |
Both budgets are enforced when a run starts as well as when it stores. A production command first sweeps the working directories of any attempt that no executor holds, so a run killed mid-attempt is cleaned up by the next one. A receipt that a live cache entry cites is never evicted, because a cached result whose receipt is gone would be a claim with nothing behind it. Losing a cache entry costs a re-run, never a result.
| Command | What it does | |
|---|---|---|
| Graph | init |
Detect Cargo, npm and Go; write sykli.json and pin it |
validate |
Check a contract without executing it | |
lock |
Re-pin a contract's hash in sykli.lock after you edit it |
|
plan |
Select the tasks a change requires; --explain shows why and what's cached |
|
run |
Execute the graph and record a receipt | |
verify |
Check a receipt against the current tree and contract | |
| Production | targets |
Discover typed artifact targets without building |
produce |
Build an artifact from captured source and run its checks | |
status · resume |
Inspect or continue a production by ID | |
diagnostics · verify-production |
Read an attempt's diagnostics; check a production record | |
| Evidence | inspect |
Collect a pull request's runs and reviews through gh |
assess |
Assess a saved evidence bundle, offline and deterministically |
Every command documents its flags and exit codes in sykli <command> --help.
- Agent guide: JSON queries, explanations, production and assessment
- CI experiment: methodology, artifacts and limitations
- Changelog: version history
- Contributing and product boundaries
- Security policy: report vulnerabilities privately
Part of False Systems · MIT