A Pkl-native task runner with a pure MoonBit implementation.
Dependencies
Typed task runner with Bazel-style incremental caching, configured in Pkl.
brew tap mizchi/pkfire https://github.com/mizchi/pkfire
brew install mizchi/pkfire/pkfcurl -fsSL https://raw.githubusercontent.com/mizchi/pkfire/main/install.sh | sh# pin a version and choose the install dir
curl -fsSL https://raw.githubusercontent.com/mizchi/pkfire/main/install.sh \
| sh -s -- --version 0.12.0 --dir /usr/local/bin
# env-var form: PKF_VERSION, PKF_INSTALL_DIR, PKF_NO_VERIFY# pick your target: linux-amd64 | linux-arm64 | darwin-arm64
target=linux-amd64
curl -fsSL -O "https://github.com/mizchi/pkfire/releases/latest/download/pkf-${target}.tar.gz"
tar -xzf "pkf-${target}.tar.gz"
install -m 0755 pkf /usr/local/bin/pkf# .github/workflows/ci.yml
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: mizchi/pkfire@v0.14.2 # or @v0 to track the latest 0.x
- run: pkf run ciWhy @v0.5.0 and not @pkfire@0.14.2? GitHub Actions cannot parse uses: <repo>@<ref> when the ref itself contains @ — the whole workflow file fails to load with a generic "workflow file issue" error and zero jobs run. Pkl release tags are pkfire@<ver> for the package URI, so the Release workflow additionally publishes v<ver> and a floating v<major> tag at the same commit. Use those from uses:. For maximum supply-chain safety, pin to the commit SHA directly: uses: mizchi/pkfire@<40-char-sha> # v0.5.0.
- uses: mizchi/pkfire@v0.14.2
- run: pkf run ci
env:
PKFIRE_REMOTE_CACHE: ${{ vars.PKFIRE_REMOTE_CACHE }}
PKFIRE_REMOTE_TOKEN: ${{ secrets.PKFIRE_REMOTE_TOKEN }}| Input | Default | Notes |
|---|---|---|
| version | the action ref, falling back to the latest release | Accepts v0.5.0, 0.4.0, v0 (floating major), or the underlying pkfire@0.14.2. Pinning via uses: mizchi/pkfire@v0.14.2 is the recommended form. |
| pkl-version | 0.32.1 | Set to none to skip the Pkl install when only pkf is needed. |
| install-dir | ${{ runner.temp }}/pkfire-bin | Both binaries are placed here; the dir is appended to GITHUB_PATH. |
| cache-pkl | false | Set to true to cache ~/.pkl/cache between runs. Useful for projects that consume remote Pkl packages (amends / import of package://pkg.pkl-lang.org/...). |
| pkl-cache-key | pkl-<hashFiles> of PklProject.deps.json + Taskfile.pkl | Override only if the default key collides across unrelated jobs in the same repo. |
nix run github:mizchi/pkfire -- run hello # one-shot
nix profile install github:mizchi/pkfire # persistentmkdir my-project && cd my-project
pkf init # writes a starter Taskfile.pkl
pkf run hello # smoke the generated taskamends "package://pkg.pkl-lang.org/github.com/mizchi/pkfire/pkfire@0.14.2#/Taskfile.pkl"
local build = new Task {
name = "build"
cmd = "moon build --target native --release"
inputs { "src/**/*.mbt"; "src/**/moon.pkg"; "moon.mod" }
outputs { "_build/native/release/build/main/main.exe" }
}
local test = new Task {
name = "test"
cmd = "moon test"
inputs { "src/**/*.mbt"; "src/**/moon.pkg"; "moon.mod" }
deps { build } // direct Task reference, typo-checked by Pkl
}
tasks { build; test }local ci = new Task {
name = "ci"
deps { build; test }
}local strict = new Task {
name = "strict"
shellFlags = List("-eu", "-o", "pipefail", "-c")
cmd = "pkl format --check ."
}
local nodeSnippet = new Task {
name = "node-snippet"
shell = "node"
shellFlags = List("-e")
cmd = "console.log(process.argv.slice(2))"
}cd services/api/internal && pkf run ci # uses services/api/Taskfile.pkl
cd /repo/root && pkf run ci # uses /repo/root/Taskfile.pklpkf list # show public tasks
pkf list --unsorted # show tasks in Taskfile declaration order
pkf list --all # include internal tasks
pkf list --color=always # force ANSI color (auto, always, never)
pkf list -v # add cmd preview and deps
pkf list --json # machine-readable (for editor / CI tooling)
pkf run test # builds first, then tests; second run hits cache
pkf run -j 8 test # cap parallelism at 8
pkf run --watch test # re-run on input changes (Ctrl+C to stop)
pkf run --dry-run test # preview: per-task hit/will-run/uncached status + cmd
pkf run --print-hash test # print action keys, do not execute
pkf run --explain-cache test # explain cache hit/miss/forced-run decisions
pkf run --no-cache test # bypass cache lookup AND store for this run
pkf run --refresh test # bypass cache lookup but DO re-store (re-baseline)
pkf up dev # start every service:true task in dev's subgraph
pkf up --watch dev # same, plus restart-on-change
pkf graph # emit Graphviz DOT for the full DAG
pkf graph --format mermaid # emit Mermaid flowchart (renders on GitHub)
pkf graph --json # machine-readable graph (tasks + edges)
pkf graph --target test # only the subgraph rooted at `test`
pkf doctor # diagnose pkf PATH, pkl/cache/remote/taskfile setup
pkf doctor --json # emit structured setup checks
pkf doctor --fix --dry-run # preview replacing stale pkf on PATH with this binary
pkf format # pkl format -w on the Taskfile's directory
pkf format --check pkl examples # exit 11 (CI-friendly) if anything is unformatted
pkf hooks install # write .git/hooks/<event> shims for matching tasks
pkf hooks list # show which hook events are wired
pkf affected --since=origin/main test # run only tasks affected by the PR diff
pkf affected --files src/main.go --explain --dry-run # inspect file -> task matches
pkf affected --check # run workflowTests declared in Taskfile.pkl
pkf run a b c # run multiple targets in one go (topological union)
pkf run # no args = the `default` task (errors if absent)
pkf run -- a b c # forward args to the `default` task when it accepts args
pkf run --timing build # also print per-task wall time at the end
pkf run 'test:*' # glob over task names (also works on affected / clean)
pkf clean # rm declared outputs of every task; --dry-run to preview
pkf cache stats # local CAS: entries, size, oldest/newest
pkf cache prune --older-than=7d # drop stale entries (--dry-run to preview)
pkf cache rm <action-key> # remove a specific entry (≥2-char prefix accepted)
pkf cache clear --yes # nuke everything (scripting-safe with --yes)
pkf run --quiet build # suppress per-task log lines (errors + summary still print)
pkf completion bash > ~/.bash_completion.d/pkf # dynamic task-name completion
pkf completion zsh > "${fpath[1]}/_pkf"
pkf completion fish > ~/.config/fish/completions/pkf.fish
pkf run --keep-going lint test # don't stop on first failure (Bazel / make -k)
pkf list --long # audit task visibility/cache/quiet/deps/io/shell flags
pkf explain build # dump every input to the action key (cache-miss debug)
pkf explain --diff old/Taskfile.pkl build # compare action-key inputs against another Taskfile
pkf run --profile=ci build # tag the run; $PKF_PROFILE + cache splits per profile
pkf run --on-fail=shell build # drop into $SHELL in the failed task's workdir on error
pkf run --remote-only build # skip local cache, only consult remote (verify remote populated)
pkf affected --watch # re-evaluate affected set on every file change
pkf graph --target build --depth=1 # show only direct deps (one hop)
pkf graph --format tree # terminal-readable dependency tree (roots only when no target)
pkf graph --format tree --target test --depth=2 # tree with deps up to two hops
pkf lint # detect dead local tasks, cache footguns, and suspicious task definitions
pkf lint --json # emit machine-readable findings for CI/editor tooling
pkf lint --fix # safely add cache = false for outputs-without-inputs findings
pkf migrate --to=0.5.0 # rewrite Taskfile.pkl's amends URI + verify
pkf pkl-cache warm # pre-populate ~/.pkl/cache (CI prefetch step)
pkf <plugin> <args> # exec `pkf-<plugin>` on PATH (git-style fallthrough)pkf graph | dot -Tsvg -o tasks.svg
pkf graph --format mermaid > tasks.mmd
pkf graph --format tree --target test{
"tasks": [
{
"name": "build",
"description": "Compile the app",
"visibility": "public",
"cmd": "moon build --target native --release",
"deps": [],
"inputs": ["src/**/*.mbt", "src/**/moon.pkg", "moon.mod"],
"outputs": ["_build/native/release/build/main/main.exe"],
"cache": true,
"workdir": "services/api",
"service": false,
"services": [],
"acceptsArgs": false,
"inheritEnv": true
}
]
}{
"tasks": [
{ "name": "build", "kind": "task", "deps": [], "cache": true },
{ "name": "ci", "kind": "aggregate", "deps": ["build"], "cache": true }
],
"edges": [
{ "from": "build", "to": "ci" }
]
}local build = new Task {
name = "build"
cmd = "moon build --target native --release"
inputs { "src/**/*.mbt"; "moon.mod" }
outputs { "_build/native/release/build/main/main.exe" }
}
local test = new Task {
name = "test"
cmd = "moon test"
inputs { "src/**/*.mbt" }
deps { build }
}
tasks { build; test }
workflowTests {
new {
name = "source edit rebuilds and retests"
changed { "src/main.mbt" }
direct { "build" }
tasks { "build"; "test" }
}
}1. host env (os.Environ()) ← inherited from the shell that ran pkf
2. defaults.Env ← Taskfile-wide common values
3. task.Env ← per-task overrides
4. resolved params (uppercased name) ← `--bump=patch` → $BUMP| Visible to cmd? | Part of the action key? | |
|---|---|---|
| host env (when inheritEnv = true, the default) | ✓ | ✗ |
| host env (when inheritEnv = false, allowlist only: PATH HOME LANG ...) | partial | ✗ |
| defaults.Env | ✓ | ✓ |
| task.Env | ✓ | ✓ |
| resolved params values ($NAME) | ✓ | ✓ (when cache = true) |
| tail args from -- a b c ($@) | ✓ | ✓ (when cache = true) |
| task.Tools | as env hints only | ✓ |
env { ["NODE_ENV"] = read("env:NODE_ENV") }Now cmd sees $NODE_ENV, AND a change to it invalidates the
cache entry. The host env still flows through for everything
else; this only promotes one value into the hashed layer.params {
new { name = "bump"; type = "enum"; choices { "patch"; "minor"; "major" }; default = "patch" }
new { name = "port"; type = "int"; default = "3000" }
new { name = "watch"; type = "bool"; default = "false" }
}Callers pass pkf run task --bump=minor --port=8080 --watch;
cmd reads $BUMP, $PORT, $WATCH. Different values cache as
different entries — usually what you want.| Option | amends line | Notes |
|---|---|---|
| Pkl package (recommended) | amends "package://pkg.pkl-lang.org/github.com/mizchi/pkfire/pkfire@0.14.2#/Taskfile.pkl" | Versioned, integrity-checked, cached by Pkl. |
| HTTPS, floating tip | amends "https://raw.githubusercontent.com/mizchi/pkfire/main/pkl/Taskfile.pkl" | What older pkf init wrote. Pkl fetches and caches. |
| HTTPS, pinned tag | amends "https://raw.githubusercontent.com/mizchi/pkfire/pkfire@0.14.2/pkl/Taskfile.pkl" | Pinned to a release tag, no package resolution. |
| Local clone | amends "../pkfire/pkl/Taskfile.pkl" | When mizchi/pkfire is a sibling checkout. |
export PKFIRE_REMOTE_CACHE=https://pkfire-cache.<account>.workers.dev
export PKFIRE_REMOTE_TOKEN=<auth token>
pkf run build # hits local first → falls back to remote → falls back to runningGET /v1/cas/<hex64> → 200 + tar.zst | 404
HEAD /v1/cas/<hex64> → 200 | 404
PUT /v1/cas/<hex64> → 201 (or 200 if already present)
Authorization: Bearer <token> (optional)| Project | What it provides |
|---|---|
| kawaz/pkf-tasks | Shared Pkl task modules published as a Pkl package: vcs/auto.pkl (jj/git runtime dispatch via abstract module + extends), docs/translations.pkl (translation-pair integrity), lint/pkl.pkl (pkl format -w). Worked example of the library-author patterns documented in skills/pkfire/SKILL.md. |
| Path | What it shows |
|---|---|
| examples/basic | Smallest possible Taskfile (one hello, one build, one test) |
| examples/node | Node project using the built-in node:test runner; zero dev deps |
| examples/rust | Single-binary Rust crate driven through cargo (fmt + clippy + test + build) |
| examples/monorepo | pnpm workspaces with one Task generated per package via a Package template |
| examples/diagnostics | list --long, lint --json/--fix, doctor --json/--fix, internal tasks, quiet output, strict shell flags |
| examples/split-import | Single entry Taskfile with task fragments under tasks/, shared constants, and typed cross-file deps |
| examples/dogfood | pkfire builds itself: cross-compile matrix + checksum + integration |
| examples/remote-cache-worker | Cloudflare Worker that backs the remote-cache protocol with R2 |
| Phase | Scope | Status |
|---|---|---|
| 0 | Pkl schema, pkl test baseline, CLI skeleton | ✅ |
| 1 | Load Taskfile.pkl via pkl-go, build DAG, run serially | ✅ |
| 2 | Parallel execution honoring deps (per-task IO capture) | ✅ |
| 3 | Action key (BLAKE3 over cmd / shell flags / env / inputs / tools / config) | ✅ |
| 4 | Local CAS, hit/miss, output restore | ✅ |
| 5 | Watch mode (pkf run --watch) | ✅ |
| 6 | Remote cache (HTTP backend + reference Cloudflare Worker) | ✅ |
| 7 | Pkl package publish (pkg.pkl-lang.org/github.com/mizchi/pkfire/pkfire) | ✅ |
| 8 | GitHub Action (mizchi/pkfire@pkfire@<ver>) + pre-built binaries on release | ✅ |
| 9 | pkf up: long-running services (service = true) with process-group cleanup and watch-driven restart | ✅ |
| 10 | services { ... } on a body task: pkf run e2e brings up live servers, runs the test, releases everything | ✅ |
| 11 | Readiness probes (readyPort / readyCmd): reuse already-running services and gate dependents on real readiness | ✅ |
| 12 | Env inheritance default + variadic tail args (acceptsArgs) + typed named params (params w/ string/enum/int/bool) + / in task names | ✅ |
moon build src/cmd/pkf --target native --release
BIN=_build/native/release/build/mizchi/pkf/src/cmd/pkf/pkf.exe
"$BIN" list # see all maintenance tasks
"$BIN" run preflight # moon check/test + pkl-test + examples + version + format
"$BIN" run conformance # contract harness: candidate vs frozen goldens (43/43)
"$BIN" run fmt # pkl format -w on Taskfile.pkl, pkl/, examples/, skills/
"$BIN" run fmt:check # formatting check without writing
"$BIN" run -f examples/dogfood/Taskfile.pkl ci # full build + integration gate# 1. Bump README + skills + recipes + PklProject. Examples are NOT
# touched here — they pin to a *published* URL and would 404 on
# `pkl eval` until the release workflow finishes.
pkf run bump --to=<new-version>
git commit -am "release: pkfire@<new-version>"
# 2. Tag locally and push. Release + v-tags workflows fire.
# The Release workflow extracts the body for the GitHub release page
# from CHANGELOG.md's `## [<new-version>]` section automatically —
# update that section BEFORE this step so the published notes match.
pkf run tag
git push origin main "pkfire@<new-version>"
# 3. After the publish workflow uploads the package, bump examples
# in a follow-up commit.
perl -i -pe 's/pkfire\@<old>/pkfire\@<new-version>/g' \
examples/basic/Taskfile.pkl examples/node/Taskfile.pkl \
examples/rust/Taskfile.pkl examples/monorepo/Taskfile.pkl \
examples/diagnostics/Taskfile.pkl examples/split-import/Taskfile.pkl \
examples/split-import/tasks/*.pkl
git commit -am "examples: bump amends URI to pkfire@<new-version>"
git pushA Pkl-native task runner with a pure MoonBit implementation.
Dependencies