Pocket User Guide
June 12, 2026 · View on GitHub
This guide covers everything you need to know to use Pocket effectively, from defining your first task to building complex CI pipelines.
Table of Contents
- Tasks
- Executing Commands
- Tool Management
- Composition
- Options
- Path Filtering
- Configuration
- Plan Introspection
- GitHub Actions Integration
- JSON Execution
Tasks
Tasks are the fundamental units of work in Pocket—linting, testing, building, deploying. Each task has a name, description, optional flags, and a body that defines what it does.
Defining Tasks
Tasks are created as struct literals:
var Hello = &pk.Task{
Name: "hello",
Usage: "print a greeting",
Do: func(ctx context.Context) error {
fmt.Println("Hello!")
return nil
},
}
For composed tasks, use the Body field instead of Do:
var Lint = &pk.Task{
Name: "lint",
Usage: "run linters",
Body: pk.Serial(Install, lintCmd()),
}
The Do Helper
The pk.Do function wraps a simple Go function func(context.Context) error
into a Runnable. This is the most common way to implement task logic.
pk.Do(func(ctx context.Context) error {
// Your task logic here
return nil
})
Hidden Tasks
If a task is only intended to be used as a dependency or called programmatically, hide it from CLI help output:
var InternalTask = &pk.Task{
Name: "internal",
Usage: "...",
Body: body,
Hidden: true,
}
Hidden tasks still execute when part of a composition tree—they just don't
appear in ./pok -h.
Manual Tasks
By default, tasks in Config.Auto run when you execute bare ./pok. If you
want a task to only run when explicitly named (e.g., ./pok deploy), add it
to Config.Manual:
var Config = &pk.Config{
Auto: pk.Serial(Lint, Test),
Manual: []pk.Runnable{Deploy},
}
Task Flags
Flags are defined as a struct with flag and usage struct tags, and accessed
at runtime with run.GetFlags[T]:
type DeployFlags struct {
Env string `flag:"env" usage:"target environment"`
}
var Deploy = &pk.Task{
Name: "deploy",
Usage: "deploy the app",
Flags: DeployFlags{Env: "staging"},
Do: func(ctx context.Context) error {
f := run.GetFlags[DeployFlags](ctx)
fmt.Printf("Deploying to %s...\n", f.Env)
return nil
},
}
Run it with:
./pok deploy -env prod
Supported field types: string, bool, int, int64, uint, uint64,
float64, time.Duration, and pointer variants (*string, *bool, etc.) for
optional overrides.
Pointer fields use nil = not set (inherits default), non-nil = explicit
override. This is useful with pk.WithFlags — you only set what you want to
change, and nil fields are skipped during diffing.
The struct approach provides compile-time safety. Field access via the returned struct means typos are caught by the compiler, not at runtime.
Suppressing Headers
By default, tasks print a :: taskname header before execution. For tasks that
output machine-readable data (e.g., JSON), suppress the header:
var Export = &pk.Task{
Name: "export",
Usage: "output JSON data",
Body: body,
HideHeader: true,
}
Executing Commands
The Exec Helper
run.Exec runs external commands with proper output handling:
pk.Do(func(ctx context.Context) error {
return run.Exec(ctx, "go", "test", "./...")
})
Output behavior:
- With
-vflag: output streams to stdout/stderr in real-time - Without
-vflag: output is captured and shown if:- The command fails, OR
- The output contains warnings (detects:
warn,deprecat,notice,caution,error)
This keeps CI logs clean while ensuring warnings and deprecation notices are never silently hidden.
To override the default notice patterns or disable detection entirely, use
pk.WithNoticePatterns(...) in your WithOptions scope:
pk.WithOptions(
NoisyTask,
pk.WithNoticePatterns(), // disable notice detection entirely
)
Other features:
- Respects context cancellation (graceful shutdown)
- Adds
.pocket/binto the command'sPATH - Sends SIGINT on cancellation (Unix), allowing graceful cleanup
Output Functions
Use these instead of fmt.Print* to ensure correct output handling in parallel
contexts:
| Function | Description |
|---|---|
run.Printf(ctx, format, args...) | Formatted output to stdout |
run.Println(ctx, args...) | Line output to stdout |
run.Errorf(ctx, format, args...) | Formatted output to stderr |
pk.Do(func(ctx context.Context) error {
run.Printf(ctx, "Processing %d items...\n", count)
return nil
})
Tool Management
One of Pocket's strengths is automated tool installation. Each tool package owns its complete lifecycle: installation, versioning, and making itself available for execution.
Tool Availability Patterns
Tools make themselves available through one of three patterns:
| Pattern | When to use | How tasks invoke |
|---|---|---|
| Symlink | Native binaries (Go, Rust, C) | run.Exec(ctx, "tool", ...) |
| Tool Exec | Standalone runtime-dependent tools | tool.Exec(ctx, ...) |
| Runtime Run | Project-managed tools (pyproject.toml, package.json) | uv.Run(ctx, opts, "tool", ...) |
Symlink pattern: The tool downloads a self-contained binary to
.pocket/tools/<tool>/<version>/ and symlinks it to .pocket/bin/. Since
run.Exec adds .pocket/bin/ to PATH, tasks can invoke it by name.
Tool Exec pattern: For tools that require a runtime (Node.js, Python), the
tool package exposes an Exec() function that handles runtime invocation
internally. No symlink is created because the tool's shebang
(#!/usr/bin/env node) would fail without the runtime on system PATH.
Runtime Run pattern: For tools defined in the project's dependency file
(pyproject.toml, package.json), use the runtime's Run() function directly. The
project controls tool versions, not Pocket.
Go Tools
Use golang.Install for Go-based tools:
import (
"github.com/fredrikaverpil/pocket/pk/run"
"github.com/fredrikaverpil/pocket/tools/golang"
)
var installLint = &pk.Task{
Name: "install:golangci-lint",
Usage: "install linter",
Body: golang.Install("github.com/golangci/golangci-lint/v2/cmd/golangci-lint", "v2.1.6"),
Hidden: true,
Global: true,
}
var Lint = &pk.Task{
Name: "lint",
Usage: "run golangci-lint",
Body: pk.Serial(
installLint,
pk.Do(func(ctx context.Context) error {
return run.Exec(ctx, "golangci-lint", "run")
}),
),
}
Install tasks use Hidden: true and Global: true:
- Hidden: Excludes from CLI help (internal implementation detail)
- Global: Deduplicates by name only, ignoring path context
Without Global: true, if Lint runs in multiple paths (via WithDetect), the
install would run once per path. With Global: true, it runs once total.
Custom Tools
For non-Go tools, use the Download API to fetch binaries from GitHub releases or
other sources. Import "github.com/fredrikaverpil/pocket/pk/download".
import "github.com/fredrikaverpil/pocket/pk/download"
var installStyLua = &pk.Task{
Name: "install:stylua",
Usage: "install StyLua formatter",
Hidden: true,
Global: true,
Body: download.Download(
fmt.Sprintf(
"https://github.com/JohnnyMorganz/StyLua/releases/download/v%s/stylua-%s-%s.zip",
"2.0.2",
pk.HostOS(),
pk.ArchToX8664(pk.HostArch()),
),
download.WithDestDir(pk.FromToolsDir("stylua", "2.0.2")),
download.WithFormat("zip"),
download.WithExtract(download.WithExtractFile(pk.BinaryName("stylua"))),
download.WithSymlink(),
download.WithSkipIfExists(pk.FromToolsDir("stylua", "2.0.2", pk.BinaryName("stylua"))),
),
}
Download API
download.Download creates a Runnable that fetches a URL and optionally
extracts it.
func Download(url string, opts ...Opt) pk.Runnable
Download Options:
| Option | Description |
|---|---|
WithDestDir(dir) | Destination directory for extraction |
WithFormat(format) | Archive format: "tar.gz", "tar", "zip", "" |
WithExtract(opt) | Add extraction options (see Extract API) |
WithSymlink() | Create symlink in .pocket/bin/ after extraction |
WithSkipIfExists(path) | Skip download if the specified file exists |
Extract API
Extraction options control how archives are unpacked:
| Option | Description |
|---|---|
WithExtractFile(name) | Extract only the specified file (by base name) |
WithRenameFile(src, dest) | Extract a file and rename it |
WithFlatten() | Flatten directory structure to destDir root |
Standalone extraction functions:
func ExtractTarGz(src, destDir string, opts ...ExtractOpt) error
func ExtractTar(src, destDir string, opts ...ExtractOpt) error
func ExtractZip(src, destDir string, opts ...ExtractOpt) error
Python Tools
Pocket provides Python tooling via the tools/uv and tasks/python packages.
Tools are installed from the project's pyproject.toml into version-specific
venvs under .pocket/venvs/.
Using the Python Task Bundle
The tasks/python package provides ready-to-use tasks for common Python
workflows:
import (
"github.com/fredrikaverpil/pocket/pk"
"github.com/fredrikaverpil/pocket/tasks/python"
)
var Config = &pk.Config{
Auto: pk.Serial(
// Format, lint, typecheck, and test with Python 3.9 (with coverage)
pk.WithOptions(
python.Tasks(),
pk.WithNameSuffix("3.9"),
pk.WithFlags(python.FormatFlags{Python: "3.9"}),
pk.WithFlags(python.LintFlags{Python: "3.9"}),
pk.WithFlags(python.TypecheckFlags{Python: "3.9"}),
pk.WithFlags(python.TestFlags{Python: "3.9", Coverage: true}),
pk.WithDetect(python.Detect()),
),
// Test against remaining Python versions (without coverage)
pk.WithOptions(
pk.Parallel(
pk.WithOptions(python.Test, pk.WithNameSuffix("3.10"), pk.WithFlags(python.TestFlags{Python: "3.10"})),
pk.WithOptions(python.Test, pk.WithNameSuffix("3.11"), pk.WithFlags(python.TestFlags{Python: "3.11"})),
pk.WithOptions(python.Test, pk.WithNameSuffix("3.12"), pk.WithFlags(python.TestFlags{Python: "3.12"})),
pk.WithOptions(python.Test, pk.WithNameSuffix("3.13"), pk.WithFlags(python.TestFlags{Python: "3.13"})),
),
pk.WithDetect(python.Detect()),
),
),
}
Configuring Python tasks:
Use pk.WithNameSuffix() to add a suffix to task names (e.g., py-test:3.9)
and pk.WithFlags() to set the Python version and enable coverage:
Available tasks:
| Task | Description |
|---|---|
python.Tasks() | All tasks: Format, Lint, Typecheck, Test |
python.Format | Format with ruff |
python.Lint | Lint with ruff (auto-fix by default) |
python.Typecheck | Type-check with mypy |
python.Test | Run pytest |
python.Detect() | DetectFunc for pyproject.toml |
When pk.WithNameSuffix("3.9") is used, tasks are automatically named with a
suffix (e.g., py-test:3.9) for CLI invocation and GitHub Actions per-task
workflow generation.
Note
Tests run without coverage by default to avoid conflicts when testing multiple
Python versions. Use python.TestFlags{Coverage: true} to enable coverage for
one version.
Venv Location
Venvs are created at .pocket/venvs/<project-path>/venv-<version>/:
- Root project:
.pocket/venvs/venv-3.9/ - Subdirectory project:
.pocket/venvs/services/api/venv-3.9/
This path-scoped approach prevents collisions in monorepos with multiple
pyproject.toml files.
Low-Level uv API
For custom Python tasks, use the tools/uv package directly:
import "github.com/fredrikaverpil/pocket/tools/uv"
var Docs = &pk.Task{
Name: "docs",
Usage: "build documentation",
Body: pk.Serial(
uv.Install,
pk.Do(func(ctx context.Context) error {
// Sync dependencies from pyproject.toml
if err := uv.Sync(ctx, uv.SyncOptions{
PythonVersion: "3.12",
AllGroups: true,
}); err != nil {
return err
}
// Run the tool from the synced environment
return uv.Run(ctx, uv.RunOptions{
PythonVersion: "3.12",
}, "mkdocs", "build")
}),
),
}
Key types and functions:
| Type/Function | Description |
|---|---|
uv.Install | Task that ensures uv is available |
uv.ContentHash(data...) | Content-addressable directory name |
uv.ExecTool(ctx, venvDir, name, args…) | Run tool via Python interpreter (no shebang) |
uv.EnsureInstalled(venvDir, name, fn) | Skip install if binary + Python exist |
uv.IsInstalled(venvDir, name) | Check tool binary + Python interpreter exist |
uv.SyncOptions | Config for uv sync (version, venv, groups) |
uv.RunOptions | Config for uv run (version, venv) |
uv.Sync(ctx, opts) | Install deps from pyproject.toml |
uv.Run(ctx, opts, cmd, args...) | Run command from synced environment |
uv.VenvPath(projectPath, pythonVersion) | Compute venv path for a project |
SyncOptions fields:
| Field | Description |
|---|---|
PythonVersion | Python version (default: uv.DefaultPythonVersion) |
VenvPath | Explicit venv path (default: auto-computed) |
ProjectDir | Where pyproject.toml lives (default: run.PathFromContext) |
AllGroups | Install all dependency groups |
Important
uv bundles Python download metadata at build time. The uv version in
tools/uv/uv.go must be recent enough to know about the Python version
specified by DefaultPythonVersion (or any PythonVersion you pass). If
Renovate bumps one independently, uv sync --python <version> will fail on
fresh CI environments with "No interpreter found for Python X.Y.Z in managed
installations or search path", while appearing to work locally from cached
Python. Run uv python list --only-downloads | grep <version> to verify
compatibility.
Standalone Python Tools
For tools managed entirely by Pocket (not from pyproject.toml), create a tool
package in tools/<toolname>/ that provides an Exec() function. This follows
the Tool Exec pattern—no symlink is created because Python scripts have
shebangs that require the runtime on PATH.
See tools/mdformat/ for a complete example:
// tools/mdformat/mdformat.go
package mdformat
//go:embed pyproject.toml
var pyprojectTOML []byte
//go:embed uv.lock
var uvLock []byte
// Version returns a content hash based on pyproject.toml, uv.lock, and Python version.
func Version() string {
return uv.ContentHash(pyprojectTOML, uvLock, []byte(uv.DefaultPythonVersion))
}
// Install ensures mdformat is available.
var Install = &pk.Task{
Name: "install:mdformat",
Usage: "install mdformat",
Body: pk.Serial(uv.Install, installMdformat()),
Hidden: true,
Global: true,
}
func installMdformat() pk.Runnable {
installDir := pk.FromToolsDir(Name, Version())
venvPath := filepath.Join(installDir, "venv")
return uv.EnsureInstalled(venvPath, Name, func(ctx context.Context) error {
os.MkdirAll(installDir, 0o755)
os.WriteFile(filepath.Join(installDir, "pyproject.toml"), pyprojectTOML, 0o644)
os.WriteFile(filepath.Join(installDir, "uv.lock"), uvLock, 0o644)
return uv.Sync(ctx, uv.SyncOptions{
PythonVersion: uv.DefaultPythonVersion,
VenvPath: venvPath,
ProjectDir: installDir,
})
})
}
// Exec runs mdformat with the given arguments.
func Exec(ctx context.Context, args ...string) error {
installDir := pk.FromToolsDir(Name, Version())
venvDir := filepath.Join(installDir, "venv")
return uv.ExecTool(ctx, venvDir, Name, args...)
}
Tasks use the tool via its Exec() function:
// tasks/markdown/format.go
var Format = &pk.Task{
Name: "md-format",
Usage: "format Markdown files",
Body: pk.Serial(mdformat.Install, pk.Do(func(ctx context.Context) error {
return mdformat.Exec(ctx, "--wrap", "80", ".")
})),
}
Project Python Tools
For tools where the project controls versions via its own
pyproject.toml—useful for linters, test runners, documentation generators, or
any tool that should match the project's Python environment:
import "github.com/fredrikaverpil/pocket/tools/uv"
var Docs = &pk.Task{
Name: "docs",
Usage: "build documentation",
Body: pk.Serial(
uv.Install,
pk.Do(func(ctx context.Context) error {
// Sync dependencies from the project's pyproject.toml.
if err := uv.Sync(ctx, uv.SyncOptions{
AllGroups: true,
}); err != nil {
return err
}
// Run the tool from the synced environment.
return uv.Run(ctx, uv.RunOptions{}, "zensical", "build")
}),
),
}
When ProjectDir is left empty, both uv.Sync and uv.Run default to
run.PathFromContext(ctx)—the directory containing the project's
pyproject.toml. The venv is stored at
.pocket/venvs/<project-path>/venv-<version>/, keeping it out of the project
tree.
No tool package under tools/ is needed—the project's lockfile controls the
version. Only uv.Install is required to ensure the uv binary is available.
When to use which pattern:
| Aspect | Standalone (.pocket/tools/) | Project-managed (.pocket/venvs/) |
|---|---|---|
| Version control | Pocket controls version | Project's pyproject.toml |
| Use case | Shared tools across projects | Project-specific tooling |
| Example | mdformat, zensical | ruff, mypy, pytest, zensical |
| Invocation | tool.Exec(ctx, ...) | uv.Run(ctx, opts, "tool", ...) |
Note
Some tools (like zensical) can be used either way. Use standalone when
Pocket should control the version. Use project-managed when the project
needs to pin its own version in pyproject.toml and uv.lock.
Node Tools
Pocket provides bun for JavaScript/TypeScript tools via the tools/bun package.
Standalone Node Tools
For tools managed entirely by Pocket, create a tool package in
tools/<toolname>/ that provides an Exec() function. Embed package.json and
bun.lock to control versions. This follows the Tool Exec pattern—no
symlink is created because Node scripts have shebangs that require Node on PATH.
See tools/prettier/ for a complete example:
// tools/prettier/prettier.go
package prettier
//go:embed package.json
var packageJSON []byte
//go:embed bun.lock
var lockfile []byte
// Version returns a content hash based on package.json and bun.lock.
func Version() string {
return bun.ContentHash(packageJSON, lockfile)
}
// Install ensures prettier is available.
var Install = &pk.Task{
Name: "install:prettier",
Usage: "install prettier",
Body: pk.Serial(bun.Install, installPrettier()),
Hidden: true,
Global: true,
}
func installPrettier() pk.Runnable {
installDir := pk.FromToolsDir(Name, Version())
return bun.EnsureInstalled(installDir, Name, func(ctx context.Context) error {
os.MkdirAll(installDir, 0o755)
os.WriteFile(filepath.Join(installDir, "package.json"), packageJSON, 0o644)
os.WriteFile(filepath.Join(installDir, "bun.lock"), lockfile, 0o644)
return bun.InstallFromLockfile(ctx, installDir)
})
}
// Exec runs prettier with the given arguments.
func Exec(ctx context.Context, args ...string) error {
installDir := pk.FromToolsDir(Name, Version())
return bun.Run(ctx, installDir, Name, args...)
}
Tasks use the tool via its Exec() function:
// tasks/markdown/format.go
var Format = &pk.Task{
Name: "md-format",
Usage: "format Markdown files",
Body: pk.Serial(prettier.Install, pk.Do(func(ctx context.Context) error {
return prettier.Exec(ctx, "--write", "**/*.md")
})),
}
Key functions:
| Function | Description |
|---|---|
bun.Install | Task that ensures bun is available |
bun.ContentHash(data...) | Content-addressable directory name |
bun.EnsureInstalled(dir, name, fn) | Skip install if binary exists |
bun.IsInstalled(installDir, name) | Check tool binary exists in node_modules |
bun.InstallFromLockfile(ctx, dir) | Install from package.json + bun.lock |
bun.BinaryPath(installDir, name) | Path to binary in node_modules/.bin |
bun.Run(ctx, installDir, pkg, args...) | Run a package via bun |
Project Node Tools
For tools where the project controls versions via package.json—useful for
build tools, test runners, or tools that should match the project's Node
environment:
import "github.com/fredrikaverpil/pocket/tools/bun"
var Build = &pk.Task{
Name: "build",
Usage: "build frontend",
Body: pk.Serial(
bun.Install,
pk.Do(func(ctx context.Context) error {
projectDir := pk.FromGitRoot("frontend")
// Install dependencies from project's package.json + bun.lock.
if err := bun.InstallFromLockfile(ctx, projectDir); err != nil {
return err
}
// Run the build script.
return bun.Run(ctx, projectDir, "build")
}),
),
}
When to use which pattern:
| Aspect | Standalone | Project-managed |
|---|---|---|
| Version control | Pocket (embedded lockfile) | Project's package.json + bun.lock |
| Storage location | .pocket/tools/<tool>/<ver>/ | Project's node_modules/ |
| Use case | Shared tools across projects | Project-specific tooling |
| Example | prettier | Build tools, test runners |
| Invocation | tool.Exec(ctx, ...) | bun.Run(ctx, dir, "tool", ...) |
Platform Helpers
These functions help construct platform-specific download URLs. All are
available directly from the pk package:
| Function | Description |
|---|---|
pk.HostOS() | Current OS: "darwin", "linux", "windows" |
pk.HostArch() | Current arch: "amd64", "arm64" |
pk.ArchToX8664(arch) | Convert amd64→x86_64, arm64→aarch64 |
pk.ArchToX64(arch) | Convert amd64→x64 |
pk.BinaryName(name) | Append .exe on Windows |
pk.OSToTitle(os) | Convert darwin→Darwin |
pk.DefaultArchiveFormat() | Returns "zip" on Windows, "tar.gz" otherwise |
Platform constants (access via pk.Darwin, pk.Linux, etc.):
pk.Darwin // "darwin"
pk.Linux // "linux"
pk.Windows // "windows"
pk.AMD64 // "amd64"
pk.ARM64 // "arm64"
pk.X8664 // "x86_64"
pk.AARCH64 // "aarch64"
pk.X64 // "x64"
Composition
Tasks are composed using Serial and Parallel combinators to build execution
trees.
Serial Execution
pk.Serial runs tasks one after another. If any task returns an error,
execution stops immediately.
var Auto = pk.Serial(
Format, // runs first
Lint, // runs second
Test, // runs third
)
Parallel Execution
pk.Parallel runs tasks concurrently. Pocket automatically buffers output so
logs don't interleave—each task's output flushes atomically when it completes.
var Auto = pk.Parallel(Lint, Test, Build)
Behavior:
- Single task in Parallel → runs without buffering (real-time output)
- Multiple tasks → buffered output, first-to-complete flushes first
- If one task fails, context is cancelled and remaining tasks exit early
Task Deduplication
The same task at the same path only runs once per invocation, even if referenced multiple times in your composition tree. This makes it safe to compose shared dependencies without redundant work.
pk.Serial(
pk.Parallel(Lint, Test), // Both depend on Install
Build, // Also depends on Install
)
// Install runs once, not three times
Referencing the same task under multiple path scopes runs it in the union of all scopes' paths, deduplicated per path:
pk.Serial(
pk.WithOptions(Lint, pk.WithPath("svc-a")),
pk.WithOptions(Lint, pk.WithPath("svc-b")),
)
// Lint runs in svc-a and svc-b
Use WithForceRun() to bypass deduplication when needed:
pk.WithOptions(
CleanTask,
pk.WithForceRun(), // Always run, even if already executed
)
Force streamed output for specific tasks, regardless of whether the -v CLI
flag was passed. Set Verbose: true directly on the task:
var Deploy = &pk.Task{
Name: "deploy",
Usage: "deploy application",
Verbose: true, // Always stream output in real-time
Do: func(ctx context.Context) error { ... },
}
Or use WithVerbose() to force it via composition:
pk.WithOptions(
DeployTask,
pk.WithVerbose(), // Always stream output in real-time
)
Options
pk.WithOptions wraps a Runnable with scoped execution options:
| Option | Description |
|---|---|
pk.WithPath(patterns...) | Only run in matching directories |
pk.WithSkipPath(patterns...) | Skip matching directories |
pk.WithSkipTask(task) | Remove a task from scope |
pk.WithSkipTask(task, patterns...) | Skip a task in matching directories |
pk.WithDetect(fn) | Auto-detect directories |
pk.WithNameSuffix(suffix) | Add suffix to task names (e.g., :v2) |
pk.WithFlags(flagsStruct) | Override a task's flags |
pk.WithForceRun() | Disable deduplication |
pk.WithVerbose() | Force verbose (streamed) output |
pk.WithNoticePatterns(...) | Override warning detection patterns |
Use pk.WithFlags() to set task flags explicitly:
pk.WithOptions(
python.Tasks(),
pk.WithNameSuffix("3.9"),
pk.WithFlags(python.FormatFlags{Python: "3.9"}),
pk.WithFlags(python.LintFlags{Python: "3.9"}),
pk.WithFlags(python.TestFlags{Python: "3.9", Coverage: true}),
pk.WithDetect(python.Detect()),
)
Note
A task referenced from multiple scopes must resolve to the same flag overrides
in all of them — conflicting WithFlags values (including an override in one
scope but not another) fail plan building. Use pk.WithNameSuffix to create
distinct variants instead.
Creating custom options: When building your own task packages, use
pk.WithFlags to set task flags:
type MyFlags struct {
Feature bool `flag:"feature" usage:"enable feature"`
}
func EnableFeature() pk.Option {
return pk.WithFlags(MyFlags{Feature: true})
}
Path Filtering
In monorepos or multi-module projects, you often want to run tasks only in specific directories. All path patterns are regular expressions.
Include and Exclude
Use pk.WithOptions to apply path constraints:
pk.WithOptions(
pk.Parallel(Lint, Test),
pk.WithPath("services/.*"), // Only in services/ subdirectories
pk.WithSkipPath("vendor"), // Skip vendor/ everywhere
)
Auto-Detection
Auto-detection scans your repository for marker files (like go.mod or
package.json) and runs tasks in those directories:
pk.WithOptions(
golang.Tasks(),
pk.WithDetect(pk.DetectByFile("go.mod")),
)
Built-in detection:
func DetectByFile(filenames ...string) DetectFunc
Pocket uses refining composition: nested WithOptions accumulate
constraints. Inner detection functions only search within directories allowed by
the outer scope.
pk.WithOptions(
pk.WithOptions(
golang.Tasks(),
pk.WithDetect(pk.DetectByFile("go.mod")),
),
pk.WithSkipPath("testdata"), // Applies to inner scope too
)
The filesystem is walked once and cached, ensuring detection is fast even in large repositories.
Task-Specific Scoping
Apply constraints to specific tasks without refactoring the tree:
| Option | Description |
|---|---|
WithSkipPath(patterns...) | Skip paths for ALL tasks in scope |
WithSkipTask(task) | Remove a task entirely from scope |
WithSkipTask(task, patterns...) | Skip a task in matching directories |
WithFlags(flagsStruct) | Set flag overrides for a task |
pk.WithOptions(
golang.Tasks(),
pk.WithSkipPath("vendor"), // Global: no tasks run in vendor/
pk.WithSkipTask(golang.Test, "foo/.*"), // Only go-test skips foo/
pk.WithSkipTask(golang.Lint), // Remove linting entirely
pk.WithFlags(golang.TestFlags{Race: true}), // Enable race detector
)
Tasks can be specified by string name or task object (recommended for type safety).
Note
WithFlags overrides only apply when the task runs as part of the composition
tree (e.g., via bare ./pok). When you invoke a task directly (e.g.,
./pok my-task), it runs with its default flag values, bypassing
composition-level overrides. If you need the flag applied for direct
invocation, pass it explicitly: ./pok my-task -flag-name value.
Shim Scoping
Pocket generates ./pok shims in directories matched by WithPath or
WithDetect.
- Running
./pokfrom root shows and executes all tasks - Running
./pokfrom a subdirectory only shows and executes tasks scoped to that path - Running
./pok <task>from a subdirectory executes that task only for the shim's path
./pok # runs all tasks across all paths
cd services/api && ./pok # only runs tasks scoped to services/api
./pok go-test # runs go-test only in services/api
Configuration
Config Struct
The main entry point for configuring Pocket:
type Config struct {
Auto Runnable // Tasks executed on bare ./pok
Manual []Runnable // Tasks only run when explicitly invoked
Plan *PlanConfig // Plan building, shims, and CI configuration
}
type PlanConfig struct {
SkipDirs []string // Directories to skip during filesystem walk
IncludeHiddenDirs bool // Include hidden directories (default: false)
Shims *ShimConfig // Which shim scripts to generate
}
Directory Skipping
Control which directories are skipped during filesystem walking:
// Default skip list (used when SkipDirs is nil)
var DefaultSkipDirs = []string{
"vendor", // Go, PHP, Ruby dependencies
"node_modules", // Node.js dependencies
"dist", // Build output
"__pycache__", // Python bytecode cache
"venv", // Python virtual environment
}
Usage:
var Config = &pk.Config{
Auto: pk.Serial(Lint, Test),
Plan: &pk.PlanConfig{
// Extend defaults
SkipDirs: append(pk.DefaultSkipDirs, "testdata", "generated"),
// Or skip nothing
// SkipDirs: []string{},
// Include hidden directories (.git, .cache, etc.)
IncludeHiddenDirs: false, // default
},
}
Shim Generation
Control which shim scripts are generated:
type ShimConfig struct {
Posix bool // pok (default)
Windows bool // pok.cmd
PowerShell bool // pok.ps1
}
Helper:
pk.AllShimsConfig() // All three shims (POSIX is the default when omitted)
Usage:
var Config = &pk.Config{
Auto: pk.Serial(Lint, Test),
Plan: &pk.PlanConfig{
Shims: pk.AllShimsConfig(), // Generate all platform shims
},
}
Git Diff Check
Pocket can run git diff --exit-code after task execution to catch unintended
file modifications. This is enabled with the -g flag:
./pok -g # Run all auto tasks, then git diff
./pok lint -g # Run lint task, then git diff
The -g flag causes Pocket to fail if there are uncommitted changes after tasks
complete. This is useful in CI to ensure generated files are up to date.
Conventional Commits Check
Pocket can validate commit messages against the
Conventional Commits format. This is
enabled with the -c flag:
./pok -c # Run all auto tasks, then validate commits
./pok lint -c # Run lint task, then validate commits
The -c flag validates commits between HEAD and the upstream tracking branch
(or the origin's default branch for new branches). Merge commits are skipped.
Each commit message must match the format type[(scope)][!]: description, where
the description must not start with uppercase.
Both flags can be combined: ./pok -g -c.
Plan Introspection
Pocket builds an execution plan before running tasks. This plan is accessible at
runtime for advanced use cases like CI workflow generation. For human
inspection, ./pok plan prints the configured tree, and
./pok plan < tree.json or ./pok plan tree.json prints a JSON task tree
without executing it.
Accessing the Plan
pk.Do(func(ctx context.Context) error {
plan := run.PlanFromContext(ctx)
if plan == nil {
return errors.New("no plan in context")
}
// Use plan for introspection
for _, task := range plan.Tasks() {
fmt.Printf("Task: %s\n", task.Name)
}
return nil
})
Plan Structure
type Plan struct {
// Internal: tree, tasks, pathMappings, moduleDirectories, shimConfig
}
// Public methods
func (p *Plan) Tasks() []TaskInfo // All tasks in the plan
func (p *Plan) ShimConfig() *ShimConfig // Resolved shim configuration
Context accessors (from the pk/run package):
| Function | Description |
|---|---|
run.PlanFromContext(ctx) | Get the Plan from context (nil if not set) |
run.PathFromContext(ctx) | Current execution path relative to git root |
run.Verbose(ctx) | Whether -v flag was provided |
Path helpers:
| Function | Description |
|---|---|
FromGitRoot(elems...) | Absolute path relative to git repository root |
FromPocketDir(elems...) | Absolute path relative to .pocket/ |
FromBinDir(elems...) | Absolute path relative to .pocket/bin/ |
FromToolsDir(elems...) | Absolute path relative to .pocket/tools/ |
GitHub Actions Integration
Pocket provides two approaches for GitHub Actions CI/CD integration. Both are
configured through github.Tasks() and pk.WithOptions.
Simple Workflow (Default)
By default, github.Tasks() generates a simple pocket.yml workflow that runs
all tasks on configured platforms:
import "github.com/fredrikaverpil/pocket/tasks/github"
var Config = &pk.Config{
Auto: pk.Parallel(
golang.Tasks(),
pk.WithOptions(
github.Tasks(),
),
),
}
This generates .github/workflows/pocket.yml:
jobs:
pocket:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- run: ./pok -v
Pros: Simple, predictable, easy to understand.
Cons: All tasks run serially; no per-task platform customization.
Per-Task Workflow
For per-task parallelism and platform customization, enable the per-task workflow. This generates static job definitions at workflow creation time—each task/platform combination becomes a separate job in the YAML file.
import "github.com/fredrikaverpil/pocket/tasks/github"
var Config = &pk.Config{
Auto: pk.Parallel(
golang.Tasks(),
pk.WithOptions(
github.Tasks(),
pk.WithFlags(github.WorkflowFlags{
PerPocketTaskJob: new(true),
Platforms: github.AllPlatforms(),
PerPocketTaskJobOptions: map[string]github.PerPocketTaskJobOption{
golang.Lint.Name: {Platforms: []github.Platform{github.Ubuntu}}, // lint only on Linux
"github-workflows": {Exclude: true},
},
}),
),
),
}
This configuration:
github.Tasks()returns theWorkflowstaskpk.WithFlags(github.WorkflowFlags{...})enables the per-task workflow and configures platforms and per-task options
Running ./pok github-workflows generates jobs like:
jobs:
go-lint-ubuntu:
name: go-lint (ubuntu-latest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... setup steps ...
- run: ./pok -v -g go-lint
go-test-ubuntu:
name: go-test (ubuntu-latest)
runs-on: ubuntu-latest
# ...
go-test-macos:
name: go-test (macos-latest)
runs-on: macos-latest
# ...
go-test-windows:
name: go-test (windows-latest)
runs-on: windows-latest
# ...
GitHub Task Configuration
Use pk.WithFlags() to configure the github.Workflows task:
type WorkflowFlags struct {
// CLI flags (can be set via command line or pk.WithFlags):
ConventionalCommitWorkflow *bool `flag:"conventional-commit-workflow" usage:"conventional commit PR"`
GhPagesWorkflow *bool `flag:"gh-pages-workflow" usage:"GitHub Pages"`
GoReleaserWorkflow *bool `flag:"goreleaser-workflow" usage:"GoReleaser release"`
PerPocketTaskJob *bool `flag:"per-pocket-task-job" usage:"per-task jobs"`
ReleasePleaseWorkflow *bool `flag:"release-please-workflow" usage:"release-please"`
StaleWorkflow *bool `flag:"stale-workflow" usage:"stale issues"`
GitDiff *bool `flag:"git-diff" usage:"check uncommitted changes"`
// Programmatic-only (set via pk.WithFlags, not available as CLI flags):
Platforms []Platform
PerPocketTaskJobOptions map[string]PerPocketTaskJobOption
}
Pointer semantics:
*boolfields usenil= inherit default,new(true)/new(false)= explicit override. This is what makesWithFlagsergonomic — you only set what you want to change.
PerPocketTaskJobOption
Per-task options are configured directly within WorkflowFlags via the
PerPocketTaskJobOptions map:
type PerPocketTaskJobOption struct {
// Platforms overrides WorkflowFlags.Platforms for this task.
Platforms []Platform
// Exclude removes this task from the per-task workflow entirely.
Exclude bool
// GitDiff overrides WorkflowFlags.GitDiff for this task.
GitDiff *bool
}
Available platforms are github.Ubuntu, github.MacOS, and github.Windows.
Use github.AllPlatforms() to get all platforms (returns []Platform).
Note
The github-workflows task also accepts CLI flags directly (e.g.,
./pok github-workflows -per-pocket-task-job). Flag overrides via
pk.WithFlags only apply when running through the composition tree (bare
./pok). When invoking tasks directly, pass flags explicitly.
Programmatic-only fields (Platforms, PerPocketTaskJobOptions) can only be
set via pk.WithFlags.
Benefits comparison:
| Feature | Simple | Per-Job |
|---|---|---|
| Per-task visibility in GitHub UI | No | Yes |
| Per-task platform configuration | No | Yes |
| Parallel task execution | No | Yes |
| Fail-fast granularity | All tasks | Per task |
| Configuration complexity | Low | Medium |
JSON Execution
In addition to the typed .pocket/config.go path, Pocket can be driven from a
JSON document. This is aimed at LLMs and agents that need to compose ad-hoc
task trees without scaffolding a Go project. The JSON path uses the exact same
engine as the Go config path — same Serial/Parallel semantics, the same
deduplication, the same output buffering, the same global flag behavior.
Schema
A versioned root with optional global execution options and a single execution
tree. Each node has an explicit type: task, command, serial, or
parallel. Unknown fields error.
{
"version": 1,
"options": {
"gitdiff": true
},
"tree": {
"type": "serial",
"children": [
{
"type": "task",
"name": "go-format"
},
{
"type": "parallel",
"children": [
{ "type": "task", "name": "go-test" },
{
"type": "command",
"name": "custom-vet",
"argv": ["go", "vet", "./..."]
}
]
}
]
}
}
options mirrors global execution flags such as -g, -s, -v, and -c.
task nodes reference existing Pocket tasks by name. command nodes run raw
argument vectors; argv[0] is the executable and the rest are arguments. Task
and command nodes accept an optional paths array of literal directories
relative to the git root. Omit paths to use the referenced task's existing
paths, or the repository root for raw commands. See the
JSON Execution reference for the full set of
validation rules.
Executing JSON
Pipe a document into the exec builtin:
echo '{"version":1,"tree":{"type":"command","argv":["echo","hello"],"name":"greet"}}' \
| ./pok exec
Global flags work the same as for any other task:
./pok -v exec < tree.json # stream task output instead of buffering
./pok -s exec < tree.json # force serial execution
./pok -g exec < tree.json # run git diff check after execution
Validation and parse errors are emitted to stderr as JSON objects, one per error, so agents can parse the failure:
{ "error": "tree.children[0].name: required for command nodes" }
The CLI exits non-zero on any validation or execution error.
Print the JSON Schema (Draft-07) for the v1 format with:
./pok exec --schema
Inspecting a Go Project as JSON
The global --json flag emits the executable task tree of the current
.pocket/config.go project, instead of executing it:
./pok --json # emit the full Auto tree as JSON
./pok --json go-test # emit a single task reference
./pok --json -g go-test # include the git-diff post-action as JSON options
Because Go-defined task bodies are not shell commands, emitted task nodes use
{ "type": "task", "name": "..." } references rather than raw argv commands.
The output is accepted by ./pok exec in the same Pocket project.