[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81704":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":12,"stars7d":12,"stars30d":12,"stars90d":15,"forks30d":15,"starsTrendScore":16,"compositeScore":17,"rankGlobal":9,"rankLanguage":9,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":9,"pushedAt":9,"updatedAt":23,"readmeContent":24,"aiSummary":25,"trendingCount":15,"starSnapshotCount":15,"syncStatus":26,"lastSyncTime":27,"discoverSource":28},81704,"pkfire","mizchi\u002Fpkfire","mizchi","Typed task runner with Bazel-style incremental caching, configured in Pkl.",null,"Go",46,1,45,6,0,3,43.5,"MIT License",false,"main",true,[],"2026-06-12 04:01:35","# pkfire\n\n[![Test](https:\u002F\u002Fgithub.com\u002Fmizchi\u002Fpkfire\u002Factions\u002Fworkflows\u002Ftest.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fmizchi\u002Fpkfire\u002Factions\u002Fworkflows\u002Ftest.yml)\n[![Nix](https:\u002F\u002Fgithub.com\u002Fmizchi\u002Fpkfire\u002Factions\u002Fworkflows\u002Fnix.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fmizchi\u002Fpkfire\u002Factions\u002Fworkflows\u002Fnix.yml)\n\n> Typed task runner with Bazel-style incremental caching, configured in [Pkl](https:\u002F\u002Fpkl-lang.org\u002F).\n\nThe name `pkfire` comes from \"Pkl task fire\": define tasks in Pkl,\nthen fire them through the `pkf` CLI.\n\n`pkfire` (CLI: `pkf`) replaces hand-written `justfile`s with a typed,\ncomposable Pkl schema. Tasks declare their inputs, outputs, and\ndependencies; `pkf` builds a DAG and executes only the steps whose\naction key has changed. Cached outputs are restored from a\ncontent-addressed store under `~\u002F.cache\u002Fpkfire`.\n\n## Why pkfire\n\npkfire competes with the same lightweight task runners you already\nreach for — `make`, [`just`](https:\u002F\u002Fgithub.com\u002Fcasey\u002Fjust), npm\nscripts, `package.json` `\"scripts\"`, `Taskfile.yml`. They all work\nfine for a handful of one-line shell commands. The pain shows up\nonce a project has:\n\n- **Shared inputs** (\"these 6 globs of `.go` files feed three\n  different tasks\") that you keep copy-pasting.\n- **Matrix duplication** — four near-identical recipes for\n  `linux-amd64`, `linux-arm64`, `darwin-amd64`, `darwin-arm64`.\n- **Per-package overrides** in a monorepo where every package has a\n  `build` and `test` step that differs only in path and toolchain.\n- **No way to verify** the runner config itself — a typo in a task\n  name only fails when you run that task, in CI, on a Friday.\n\nThese tools are string-based: every task is shell, every value is\ntext, every reference is by name. They have no notion of \"this\nidentifier should resolve to a Task that already exists\". So they\nduplicate.\n\npkfire describes the same tasks in [Pkl](https:\u002F\u002Fpkl-lang.org\u002F),\nwhich is a typed configuration language with template inheritance\n(`amends`), per-module testing (`pkl test`), and ordinary functions.\nA cross-compile matrix becomes a one-line `local function\nbuildTask(p)` that the schema invokes for each platform — see\n[`examples\u002Fdogfood\u002F`](.\u002Fexamples\u002Fdogfood\u002FTaskfile.pkl), where four\nnear-duplicate `just` recipes were collapsed into a single template.\nRenaming a task in one place updates every reference; misspelling a\ndependency fails at evaluation time, before the runner starts.\n\nOn top of the language layer, pkfire adds the parts a string-based\nrunner can't: a content-addressed cache keyed on inputs\u002Fcmd\u002Fenv, an\nHTTP remote cache so CI and teammates can share hits, and a watch\nmode that reruns only the affected subgraph.\n\n## Install\n\n### Go\n\n```sh\ngo install github.com\u002Fmizchi\u002Fpkfire\u002Fcmd\u002Fpkf@latest\n```\n\nYou also need the Pkl CLI (`pkl`) on `PATH`; install it from\n[pkl-lang.org](https:\u002F\u002Fpkl-lang.org\u002Fmain\u002Fcurrent\u002Fpkl-cli\u002F) or via your\npackage manager.\n\n### GitHub Actions\n\nA setup-only composite action lives at the repo root:\n\n```yaml\n# .github\u002Fworkflows\u002Fci.yml\njobs:\n  ci:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v5\n      - uses: mizchi\u002Fpkfire@v0.11.0       # or @v0 to track the latest 0.x\n      - run: pkf run ci\n```\n\nThe action downloads the matching `pkf` binary and the Pkl CLI for\nthe runner (`linux\u002Fdarwin × amd64\u002Farm64`) and adds them to `PATH`.\nAfter it runs, the rest of the workflow calls `pkf` directly — no\n`go install`, no Pkl bootstrap.\n\n> **Why `@v0.5.0` and not `@pkfire@0.11.0`?** GitHub Actions cannot\n> parse `uses: \u003Crepo>@\u003Cref>` when the ref itself contains `@` — the\n> whole workflow file fails to load with a generic\n> \"workflow file issue\" error and zero jobs run. Pkl release tags\n> are `pkfire@\u003Cver>` for the package URI, so the Release workflow\n> additionally publishes `v\u003Cver>` and a floating `v\u003Cmajor>` tag at\n> the same commit. Use those from `uses:`. For maximum supply-chain\n> safety, pin to the commit SHA directly:\n> `uses: mizchi\u002Fpkfire@\u003C40-char-sha> # v0.5.0`.\n\nPin the action ref to a release tag so the action code, the `pkf`\nbinary, and the Pkl schema all move together. To share cache hits\nacross CI runs and developers, wire the remote cache env:\n\n```yaml\n      - uses: mizchi\u002Fpkfire@v0.11.0\n      - run: pkf run ci\n        env:\n          PKFIRE_REMOTE_CACHE: ${{ vars.PKFIRE_REMOTE_CACHE }}\n          PKFIRE_REMOTE_TOKEN: ${{ secrets.PKFIRE_REMOTE_TOKEN }}\n```\n\nInputs:\n\n| Input | Default | Notes |\n| --- | --- | --- |\n| `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.11.0`. Pinning via `uses: mizchi\u002Fpkfire@v0.11.0` is the recommended form. |\n| `pkl-version` | `0.31.1` | Set to `none` to skip the Pkl install when only `pkf` is needed. |\n| `install-dir` | `${{ runner.temp }}\u002Fpkfire-bin` | Both binaries are placed here; the dir is appended to `GITHUB_PATH`. |\n| `cache-pkl` | `false` | Set to `true` to cache `~\u002F.pkl\u002Fcache` between runs. Useful for projects that consume remote Pkl packages (`amends` \u002F `import` of `package:\u002F\u002Fpkg.pkl-lang.org\u002F...`). |\n| `pkl-cache-key` | `pkl-\u003ChashFiles>` of `PklProject.deps.json` + `Taskfile.pkl` | Override only if the default key collides across unrelated jobs in the same repo. |\n\n### Nix (no Go toolchain required)\n\n```sh\nnix run github:mizchi\u002Fpkfire -- run hello       # one-shot\nnix profile install github:mizchi\u002Fpkfire        # persistent\n```\n\nThe flake builds the `pkf` binary and wraps it so the bundled Pkl CLI\nis on `PATH` automatically — end users do not install Go or Pkl\nthemselves. The Nix workflow on every push to `main` and on every PR\nverifies the flake builds cleanly on `aarch64-darwin` and\n`x86_64-linux` runners; the badge above tracks its status.\n\n`nix develop` opens a shell with `go`, `pkl`, and `gopls` for working\non pkfire itself.\n\n## Quick start\n\n```sh\nmkdir my-project && cd my-project\npkf init                # writes a starter Taskfile.pkl\npkf run hello           # smoke the generated task\n```\n\n`pkf init` writes a Taskfile that `amends` the schema over HTTPS, so\nyour project does not need a clone of this repo.\n\n## Authoring a Taskfile\n\n```pkl\namends \"package:\u002F\u002Fpkg.pkl-lang.org\u002Fgithub.com\u002Fmizchi\u002Fpkfire\u002Fpkfire@0.11.0#\u002FTaskfile.pkl\"\n\nlocal build = new Task {\n  name = \"build\"\n  cmd = \"go build -o bin\u002Fapp .\u002Fcmd\u002Fapp\"\n  inputs { \"**\u002F*.go\"; \"go.mod\"; \"go.sum\" }\n  outputs { \"bin\u002Fapp\" }\n}\n\nlocal test = new Task {\n  name = \"test\"\n  cmd = \"go test .\u002F...\"\n  inputs { \"**\u002F*.go\" }\n  deps { build }            \u002F\u002F direct Task reference, typo-checked by Pkl\n}\n\ntasks { build; test }\n```\n\nEach task is a `Task` instance with a unique `name`. Dependencies are\n*Task references* (`deps { build }`), not strings — referencing an\nundefined task fails at Pkl evaluation time with a name-resolution\nerror, before the runner ever starts. Renaming a task in one place\nupdates every reference automatically.\n\nTasks that only aggregate dependencies can omit `cmd`:\n\n```pkl\nlocal ci = new Task {\n  name = \"ci\"\n  deps { build; test }\n}\n```\n\n`cmd` runs as `\u003Cshell> \u003CshellFlags...> \u003Ccmd>`. The default is\n`shell = \"bash\"` and `shellFlags = List(\"-c\")`; override\n`shellFlags` for strict mode or non-`-c` runtimes:\n\n```pkl\nlocal strict = new Task {\n  name = \"strict\"\n  shellFlags = List(\"-eu\", \"-o\", \"pipefail\", \"-c\")\n  cmd = \"pkl format --check .\"\n}\n\nlocal nodeSnippet = new Task {\n  name = \"node-snippet\"\n  shell = \"node\"\n  shellFlags = List(\"-e\")\n  cmd = \"console.log(process.argv.slice(2))\"\n}\n```\n\nFor wrapper tasks whose stdout\u002Fstderr is the product, set\n`quiet = true` to suppress pkfire's per-task diagnostic lines without\nhiding the command's own output.\n\nWhen `-f` is not supplied, `pkf` walks up from the current directory\nto find the nearest `Taskfile.pkl` (the same discovery rule git uses\nfor `.git\u002F`), so any of these works the same:\n\n```sh\ncd services\u002Fapi\u002Finternal && pkf run ci    # uses services\u002Fapi\u002FTaskfile.pkl\ncd \u002Frepo\u002Froot && pkf run ci               # uses \u002Frepo\u002Froot\u002FTaskfile.pkl\n```\n\n```sh\npkf list                       # show public tasks\npkf list --unsorted            # show tasks in Taskfile declaration order\npkf list --all                 # include internal tasks\npkf list --color=always        # force ANSI color (auto, always, never)\npkf list -v                    # add cmd preview and deps\npkf list --json                # machine-readable (for editor \u002F CI tooling)\npkf run test                   # builds first, then tests; second run hits cache\npkf run -j 8 test              # cap parallelism at 8\npkf run --watch test           # re-run on input changes (Ctrl+C to stop)\npkf run --dry-run test         # preview: per-task hit\u002Fwill-run\u002Funcached status + cmd\npkf run --print-hash test      # print action keys, do not execute\npkf run --explain-cache test   # explain cache hit\u002Fmiss\u002Fforced-run decisions\npkf run --no-cache test        # bypass cache lookup AND store for this run\npkf run --refresh test         # bypass cache lookup but DO re-store (re-baseline)\npkf up dev                     # start every service:true task in dev's subgraph\npkf up --watch dev             # same, plus restart-on-change\npkf graph                      # emit Graphviz DOT for the full DAG\npkf graph --format mermaid     # emit Mermaid flowchart (renders on GitHub)\npkf graph --json               # machine-readable graph (tasks + edges)\npkf graph --target test        # only the subgraph rooted at `test`\npkf doctor                     # diagnose pkf PATH, pkl\u002Fcache\u002Fremote\u002Ftaskfile setup\npkf doctor --json              # emit structured setup checks\npkf doctor --fix --dry-run     # preview replacing stale pkf on PATH with this binary\npkf format                     # pkl format -w on the Taskfile's directory\npkf format --check pkl examples # exit 11 (CI-friendly) if anything is unformatted\npkf hooks install              # write .git\u002Fhooks\u002F\u003Cevent> shims for matching tasks\npkf hooks list                 # show which hook events are wired\npkf affected --since=origin\u002Fmain test  # run only tasks affected by the PR diff\npkf affected --files src\u002Fmain.go --explain --dry-run  # inspect file -> task matches\npkf affected --check           # run workflowTests declared in Taskfile.pkl\npkf run a b c                  # run multiple targets in one go (topological union)\npkf run                        # no args = the `default` task (errors if absent)\npkf run -- a b c               # forward args to the `default` task when it accepts args\npkf run --timing build         # also print per-task wall time at the end\npkf run 'test:*'               # glob over task names (also works on affected \u002F clean)\npkf clean                      # rm declared outputs of every task; --dry-run to preview\npkf cache stats                # local CAS: entries, size, oldest\u002Fnewest\npkf cache prune --older-than=7d  # drop stale entries (--dry-run to preview)\npkf cache rm \u003Caction-key>      # remove a specific entry (≥2-char prefix accepted)\npkf cache clear --yes          # nuke everything (scripting-safe with --yes)\npkf run --quiet build          # suppress per-task log lines (errors + summary still print)\npkf completion bash > ~\u002F.bash_completion.d\u002Fpkf  # dynamic task-name completion\npkf completion zsh > \"${fpath[1]}\u002F_pkf\"\npkf completion fish > ~\u002F.config\u002Ffish\u002Fcompletions\u002Fpkf.fish\npkf run --keep-going lint test # don't stop on first failure (Bazel \u002F make -k)\npkf list --long                # audit task visibility\u002Fcache\u002Fquiet\u002Fdeps\u002Fio\u002Fshell flags\npkf explain build              # dump every input to the action key (cache-miss debug)\npkf explain --diff old\u002FTaskfile.pkl build  # compare action-key inputs against another Taskfile\npkf run --profile=ci build     # tag the run; $PKF_PROFILE + cache splits per profile\npkf run --on-fail=shell build  # drop into $SHELL in the failed task's workdir on error\npkf run --remote-only build    # skip local cache, only consult remote (verify remote populated)\npkf affected --watch           # re-evaluate affected set on every file change\npkf graph --target build --depth=1   # show only direct deps (one hop)\npkf graph --format tree        # terminal-readable dependency tree (roots only when no target)\npkf graph --format tree --target test --depth=2  # tree with deps up to two hops\npkf lint                       # detect dead local tasks, cache footguns, and suspicious task definitions\npkf lint --json                # emit machine-readable findings for CI\u002Feditor tooling\npkf lint --fix                 # safely add cache = false for outputs-without-inputs findings\npkf migrate --to=0.5.0         # rewrite Taskfile.pkl's amends URI + verify\npkf pkl-cache warm             # pre-populate ~\u002F.pkl\u002Fcache (CI prefetch step)\npkf \u003Cplugin> \u003Cargs>            # exec `pkf-\u003Cplugin>` on PATH (git-style fallthrough)\n```\n\nInside `cmd`, three env vars are always injected so tasks can\nreference their own context without hardcoding paths:\n\n- `PKF_TASK_NAME` — the task's `name`.\n- `PKF_TASK_ROOT` — absolute path to the task's `workdir` (or the\n  Taskfile directory when `workdir` is null).\n- `PKF_WORKSPACE_ROOT` — absolute path to the Taskfile's directory.\n\nThese are NOT part of the action key — they're constants of the\ntask definition, already implicit in the hash via cmd \u002F env \u002F inputs.\n\nFor cache debugging, `pkf run --explain-cache \u003Ctask>` prints each task's\naction key, cache decision, declared outputs, matched input file count,\nand input patterns that matched no files. Use `pkf explain \u003Ctask>` when\nyou need the full component-by-component action-key dump.\n\nVisualizing a Taskfile is a single pipeline:\n\n```sh\npkf graph | dot -Tsvg -o tasks.svg\npkf graph --format mermaid > tasks.mmd\npkf graph --format tree --target test\n```\n\n### Machine-readable introspection\n\nUse `pkf list --json` when tooling needs the task inventory, and\n`pkf graph --json` when it also needs dependency edges. Both commands\nrespect visibility by default; pass `--all` to include internal tasks\nand `--unsorted` to preserve Taskfile declaration order.\n\n`pkf list --json` emits:\n\n```json\n{\n  \"tasks\": [\n    {\n      \"name\": \"build\",\n      \"description\": \"Compile the app\",\n      \"visibility\": \"public\",\n      \"cmd\": \"go build -o bin\u002Fapp .\u002Fcmd\u002Fapp\",\n      \"deps\": [],\n      \"inputs\": [\"**\u002F*.go\", \"go.mod\", \"go.sum\"],\n      \"outputs\": [\"bin\u002Fapp\"],\n      \"cache\": true,\n      \"workdir\": \"services\u002Fapi\",\n      \"service\": false,\n      \"services\": [],\n      \"acceptsArgs\": false,\n      \"inheritEnv\": true\n    }\n  ]\n}\n```\n\n`pkf graph --json` emits the same task metadata, plus `kind` and\n`edges`:\n\n```json\n{\n  \"tasks\": [\n    { \"name\": \"build\", \"kind\": \"task\", \"deps\": [], \"cache\": true },\n    { \"name\": \"ci\", \"kind\": \"aggregate\", \"deps\": [\"build\"], \"cache\": true }\n  ],\n  \"edges\": [\n    { \"from\": \"build\", \"to\": \"ci\" }\n  ]\n}\n```\n\nTask `kind` is one of `task`, `aggregate`, `service`, or `noop`.\nGraph `edges` point from dependency to dependent.\n\n### Testing affected workflows\n\nWhen you first write `inputs`, `outputs`, and `deps`, pin the expected\nfile-change workflow next to the tasks:\n\n```pkl\nlocal build = new Task {\n  name = \"build\"\n  cmd = \"go build .\u002F...\"\n  inputs { \"src\u002F**\u002F*.go\"; \"go.mod\" }\n  outputs { \"bin\u002Fapp\" }\n}\n\nlocal test = new Task {\n  name = \"test\"\n  cmd = \"go test .\u002F...\"\n  inputs { \"tests\u002F**\u002F*.go\" }\n  deps { build }\n}\n\ntasks { build; test }\n\nworkflowTests {\n  new {\n    name = \"source edit rebuilds and retests\"\n    changed { \"src\u002Fmain.go\" }\n    direct { \"build\" }\n    tasks { \"build\"; \"test\" }\n  }\n}\n```\n\n`pkf affected --check` runs those cases without executing task\ncommands. For ad-hoc debugging, use\n`pkf affected --files src\u002Fmain.go --explain --dry-run` to see which\ninput pattern matched and which tasks would be in the run plan. The\nreverse view is `pkf explain test`: it now prints declared deps,\ndependents, input patterns, outputs, and the upstream input patterns\nthat can make the task affected.\n\n## Environment, args, and the action key\n\nThis section is the part that *trips up automated agents* — the rules\nlook obvious once stated but they look the wrong way around if you\nguess. Read once, refer back as needed.\n\n### Layer order (later wins)\n\nEvery `cmd` runs against an env merged from four layers:\n\n```\n1. host env (os.Environ())             ← inherited from the shell that ran pkf\n2. defaults.Env                        ← Taskfile-wide common values\n3. task.Env                            ← per-task overrides\n4. resolved params (uppercased name)   ← `--bump=patch` → $BUMP\n```\n\nPlus, when `acceptsArgs = true`, anything after `--` on the command\nline is forwarded as `$1`, `$2`, ..., `\"$@\"`.\n\n### Two contracts that are NOT the same\n\n| | Visible to `cmd`? | Part of the action key? |\n| --- | :---: | :---: |\n| host env (when `inheritEnv = true`, the default) | ✓ | ✗ |\n| host env (when `inheritEnv = false`, allowlist only: `PATH HOME LANG ...`) | partial | ✗ |\n| `defaults.Env` | ✓ | ✓ |\n| `task.Env` | ✓ | ✓ |\n| resolved `params` values (`$NAME`) | ✓ | ✓ (when `cache = true`) |\n| tail args from `-- a b c` (`$@`) | ✓ | ✓ (when `cache = true`) |\n| `task.Tools` | as env hints only | ✓ |\n\nThe mismatch on the \"host env\" row is deliberate. `cmd` should be\nable to use `SSH_AUTH_SOCK`, `GPG_AGENT_INFO`, your `LANG`, your\neditor — without those silently busting cache the next time you\nssh-add a different key. Only schema-declared layers participate in\nthe action key.\n\n### When to use what\n\n- **You want `cmd` to see a host env var.** Default state. Do\n  nothing — `inheritEnv = true` already passes everything through.\n- **You want a host env var to *also* affect cache.** Read it into\n  `task.Env` explicitly:\n  ```pkl\n  env { [\"NODE_ENV\"] = read(\"env:NODE_ENV\") }\n  ```\n  Now `cmd` sees `$NODE_ENV`, AND a change to it invalidates the\n  cache entry. The host env still flows through for everything\n  else; this only promotes one value into the hashed layer.\n- **You want hermetic builds** (release pipelines, reproducibility-\n  sensitive CI). Set `inheritEnv = false` per task. `cmd` then sees\n  only the tiny allowlist plus whatever you put in `env { ... }`,\n  and the action key fully describes the env.\n- **You want runtime input that changes per invocation** (a port, a\n  bump kind, a watch flag). Declare `params { ... }`:\n  ```pkl\n  params {\n    new { name = \"bump\"; type = \"enum\"; choices { \"patch\"; \"minor\"; \"major\" }; default = \"patch\" }\n    new { name = \"port\"; type = \"int\";  default = \"3000\" }\n    new { name = \"watch\"; type = \"bool\"; default = \"false\" }\n  }\n  ```\n  Callers pass `pkf run task --bump=minor --port=8080 --watch`;\n  `cmd` reads `$BUMP`, `$PORT`, `$WATCH`. Different values cache as\n  different entries — usually what you want.\n- **You want variadic positional args** (the `just *ARGS` shape).\n  Set `acceptsArgs = true` and write `cmd = \"node \\\"$@\\\"\"`. Callers\n  pass `pkf run task -- a b c`. The args fold into the action key,\n  so command wrappers typically also set `cache = false`.\n- **You want helper tasks hidden from normal discovery.** Set\n  `visibility = \"internal\"`. `pkf list` and `pkf graph` hide it by\n  default, `--all` reveals it, and `pkf run \u003Cname>` can still execute\n  it directly.\n\n### Things that confuse agents\n\n- **`read(\"env:X\")` is NOT how you read host env at runtime.** It is\n  Pkl-evaluation-time interpolation: the *value at the time pkf\n  evaluated the Taskfile* gets baked into the rendered task. That\n  is exactly what you want when you want the value to affect the\n  action key, but if you only need `cmd` to see the var, plain\n  inheritance is enough — don't write `read(\"env:...\")` for\n  ergonomics-only env like `SSH_AUTH_SOCK`.\n- **`$VAR` inside `cmd` is shell expansion, not Pkl interpolation.**\n  Write `cmd = \"echo $HOME\"` — pkfire passes the literal string to\n  bash, bash expands `$HOME` from the merged env. Pkl's `\\(...)`\n  interpolation runs at schema evaluation time and bakes a\n  constant into the rendered task — useful occasionally but rarely\n  what you want for env vars.\n- **`acceptsArgs = false` is the default for a reason.** A task\n  that silently absorbs whatever comes after its name is a typo\n  vector. Opt in only for command wrappers (`script`, `test\n  --grep=...`, etc.).\n- **`bool` params do not consume the next token.** `--watch\n  --port=80` parses as `WATCH=true PORT=80`. Use `--watch=false`\n  for explicit negation. (`int`, `string`, `enum` *do* take the\n  next token when written without `=`.)\n\n## Pointing at the schema\n\nThe `Taskfile.pkl` schema lives in this repo. From a downstream project\npick whichever option fits:\n\n| Option | `amends` line | Notes |\n| --- | --- | --- |\n| Pkl package (recommended) | `amends \"package:\u002F\u002Fpkg.pkl-lang.org\u002Fgithub.com\u002Fmizchi\u002Fpkfire\u002Fpkfire@0.11.0#\u002FTaskfile.pkl\"` | Versioned, integrity-checked, cached by Pkl. |\n| HTTPS, floating tip | `amends \"https:\u002F\u002Fraw.githubusercontent.com\u002Fmizchi\u002Fpkfire\u002Fmain\u002Fpkl\u002FTaskfile.pkl\"` | What older `pkf init` wrote. Pkl fetches and caches. |\n| HTTPS, pinned tag | `amends \"https:\u002F\u002Fraw.githubusercontent.com\u002Fmizchi\u002Fpkfire\u002Fpkfire@0.11.0\u002Fpkl\u002FTaskfile.pkl\"` | Pinned to a release tag, no package resolution. |\n| Local clone | `amends \"..\u002Fpkfire\u002Fpkl\u002FTaskfile.pkl\"` | When `mizchi\u002Fpkfire` is a sibling checkout. |\n\nThe package is published as a GitHub release whose tag matches\n`pkfire@\u003Cversion>`. `pkg.pkl-lang.org` redirects the URI above to the\nrelease zip — see `pkl\u002FPklProject` for the metadata and\n`.github\u002Fworkflows\u002Fpkl-publish.yml` for the publish flow.\n\n## Remote cache\n\nSet `PKFIRE_REMOTE_CACHE` (and optionally `PKFIRE_REMOTE_TOKEN`) to point\n`pkf` at any HTTP server that speaks the cache protocol — the local CAS\nbecomes a write-through layer, and a teammate \u002F CI runner that has never\nbuilt before can restore artifacts from the remote on its first run.\n\n```sh\nexport PKFIRE_REMOTE_CACHE=https:\u002F\u002Fpkfire-cache.\u003Caccount>.workers.dev\nexport PKFIRE_REMOTE_TOKEN=\u003Cauth token>\npkf run build    # hits local first → falls back to remote → falls back to running\n```\n\nThe reference backend is a 60-line Cloudflare Worker that stores blobs\nin R2 and runs a daily TTL-based GC; see\n[`examples\u002Fremote-cache-worker\u002F`](.\u002Fexamples\u002Fremote-cache-worker\u002F).\n\nProtocol summary:\n\n```\nGET  \u002Fv1\u002Fcas\u002F\u003Chex64>   → 200 + tar.zst | 404\nHEAD \u002Fv1\u002Fcas\u002F\u003Chex64>   → 200 | 404\nPUT  \u002Fv1\u002Fcas\u002F\u003Chex64>   → 201 (or 200 if already present)\nAuthorization: Bearer \u003Ctoken>   (optional)\n```\n\n## Skill\n\nIf you author Pkl tasks with help from a Claude Code agent (or any\nsimilar tool that consumes APM-style skills), point it at\n[`skills\u002Fpkfire\u002FSKILL.md`](.\u002Fskills\u002Fpkfire\u002FSKILL.md). It documents the\nschema, the typed-`deps` model, the cache semantics, and the common\npitfalls, plus copy-paste recipes under\n[`skills\u002Fpkfire\u002Fassets\u002Frecipes\u002F`](.\u002Fskills\u002Fpkfire\u002Fassets\u002Frecipes\u002F) for\nbuild\u002Ftest, split\u002Fimport, services, hooks, diagnostics, and cache\nworkflows.\n\n## Used by\n\nReal-world consumers building on top of the schema or the action.\nOpen a PR to add yours.\n\n| Project | What it provides |\n| --- | --- |\n| [kawaz\u002Fpkf-tasks](https:\u002F\u002Fgithub.com\u002Fkawaz\u002Fpkf-tasks) | Shared Pkl task modules published as a Pkl package: `vcs\u002Fauto.pkl` (jj\u002Fgit runtime dispatch via abstract module + extends), `docs\u002Ftranslations.pkl` (translation-pair integrity), `lint\u002Fpkl.pkl` (`pkl format -w`). Worked example of the library-author patterns documented in [`skills\u002Fpkfire\u002FSKILL.md`](.\u002Fskills\u002Fpkfire\u002FSKILL.md). |\n\n## Examples\n\n| Path | What it shows |\n| --- | --- |\n| [`examples\u002Fbasic`](.\u002Fexamples\u002Fbasic\u002FTaskfile.pkl) | Smallest possible Taskfile (one `hello`, one `build`, one `test`) |\n| [`examples\u002Fnode`](.\u002Fexamples\u002Fnode\u002F) | Node project using the built-in `node:test` runner; zero dev deps |\n| [`examples\u002Frust`](.\u002Fexamples\u002Frust\u002F) | Single-binary Rust crate driven through `cargo` (fmt + clippy + test + build) |\n| [`examples\u002Fmonorepo`](.\u002Fexamples\u002Fmonorepo\u002F) | pnpm workspaces with one Task generated per package via a `Package` template |\n| [`examples\u002Fdiagnostics`](.\u002Fexamples\u002Fdiagnostics\u002F) | `list --long`, `lint --json\u002F--fix`, `doctor --json\u002F--fix`, internal tasks, quiet output, strict shell flags |\n| [`examples\u002Fsplit-import`](.\u002Fexamples\u002Fsplit-import\u002F) | Single entry Taskfile with task fragments under `tasks\u002F`, shared constants, and typed cross-file deps |\n| [`examples\u002Fdogfood`](.\u002Fexamples\u002Fdogfood\u002FTaskfile.pkl) | pkfire builds itself: cross-compile matrix + checksum + integration |\n| [`examples\u002Fremote-cache-worker`](.\u002Fexamples\u002Fremote-cache-worker\u002F) | Cloudflare Worker that backs the remote-cache protocol with R2 |\n\n## Status\n\n| Phase | Scope | Status |\n| --- | --- | --- |\n| 0 | Pkl schema, `pkl test` baseline, CLI skeleton | ✅ |\n| 1 | Load `Taskfile.pkl` via `pkl-go`, build DAG, run serially | ✅ |\n| 2 | Parallel execution honoring `deps` (per-task IO capture) | ✅ |\n| 3 | Action key (BLAKE3 over cmd \u002F shell flags \u002F env \u002F inputs \u002F tools \u002F config) | ✅ |\n| 4 | Local CAS, hit\u002Fmiss, output restore | ✅ |\n| 5 | Watch mode (`pkf run --watch`) | ✅ |\n| 6 | Remote cache (HTTP backend + reference Cloudflare Worker) | ✅ |\n| 7 | Pkl package publish (`pkg.pkl-lang.org\u002Fgithub.com\u002Fmizchi\u002Fpkfire\u002Fpkfire`) | ✅ |\n| 8 | GitHub Action (`mizchi\u002Fpkfire@pkfire@\u003Cver>`) + pre-built binaries on release | ✅ |\n| 9 | `pkf up`: long-running services (`service = true`) with process-group cleanup and watch-driven restart | ✅ |\n| 10 | `services { ... }` on a body task: `pkf run e2e` brings up live servers, runs the test, releases everything | ✅ |\n| 11 | Readiness probes (`readyPort` \u002F `readyCmd`): reuse already-running services and gate dependents on real readiness | ✅ |\n| 12 | Env inheritance default + variadic tail args (`acceptsArgs`) + typed named params (`params` w\u002F string\u002Fenum\u002Fint\u002Fbool) + `\u002F` in task names | ✅ |\n\n## Development\n\npkfire dogfoods itself: the repo's own `Taskfile.pkl` declares the\nmaintenance tasks, and the cross-compile \u002F integration matrix lives\nin `examples\u002Fdogfood\u002FTaskfile.pkl`. Both work with the `pkf` binary\nyou'd install for any other project.\n\n```sh\ngo install .\u002Fcmd\u002Fpkf\n\npkf list                                      # see all maintenance tasks\npkf run preflight                             # vet + go-test + pkl-test + examples + version + format\npkf run test:race                             # go test -race .\u002F...\npkf run fmt                                   # pkl format -w on Taskfile.pkl, pkl\u002F, examples\u002F, skills\u002F\npkf run fmt:check                             # formatting check without writing\npkf run -f examples\u002Fdogfood\u002FTaskfile.pkl ci   # full release gate (cross-compile + integration)\n```\n\nTo cut a Pkl package release:\n\n```sh\n# 1. Bump README + skills + recipes + PklProject. Examples are NOT\n#    touched here — they pin to a *published* URL and would 404 on\n#    `pkl eval` until the release workflow finishes.\npkf run bump --to=\u003Cnew-version>\ngit commit -am \"release: pkfire@\u003Cnew-version>\"\n\n# 2. Tag locally and push. Release + v-tags workflows fire.\n# The Release workflow extracts the body for the GitHub release page\n# from CHANGELOG.md's `## [\u003Cnew-version>]` section automatically —\n# update that section BEFORE this step so the published notes match.\npkf run tag\ngit push origin main \"pkfire@\u003Cnew-version>\"\n\n# 3. After the publish workflow uploads the package, bump examples\n#    in a follow-up commit.\nperl -i -pe 's\u002Fpkfire\\@\u003Cold>\u002Fpkfire\\@\u003Cnew-version>\u002Fg' \\\n  examples\u002Fbasic\u002FTaskfile.pkl examples\u002Fnode\u002FTaskfile.pkl \\\n  examples\u002Frust\u002FTaskfile.pkl examples\u002Fmonorepo\u002FTaskfile.pkl \\\n  examples\u002Fdiagnostics\u002FTaskfile.pkl examples\u002Fsplit-import\u002FTaskfile.pkl \\\n  examples\u002Fsplit-import\u002Ftasks\u002F*.pkl\ngit commit -am \"examples: bump amends URI to pkfire@\u003Cnew-version>\"\ngit push\n```\n\n`pkf run check-version` (wrapping\n`scripts\u002Fcheck-version-consistency.sh`) covers the in-flight\nschema version across README + skills + recipes. Examples are\nexcluded for the publish-order reason above.\n\n## License\n\nMIT — see [LICENSE](.\u002FLICENSE).\n","pkfire 是一个基于 Pkl 语言配置的类型化任务运行器，支持 Bazel 风格的增量缓存。它使用 Go 语言开发，通过 Pkl 文件定义任务，并利用 `pkf` 命令行工具执行。其核心功能包括输入、输出和依赖关系声明，构建有向无环图（DAG）并仅执行更改的部分，从而提高效率；同时提供内容寻址的本地缓存以及 HTTP 远程缓存机制，方便团队成员共享缓存结果。适用于需要处理复杂任务依赖、跨平台编译矩阵或在大型代码库中进行任务管理的场景，特别是当项目面临共享输入、重复矩阵配置等问题时，pkfire 可以显著简化配置并减少错误。",2,"2026-06-11 04:06:01","CREATED_QUERY"]