agentsclimarketplace

Zuke write build

Skill zuke-build/zuke/plugins/zuke/skills/zuke-write-build

Write or edit a Zuke build (zuke.ts) — the code-first, strongly-typed build system for Deno/TypeScript. Use when adding or changing targets, wiring dependencies, calling a tool wrapper (DenoTasks, NpmTasks, DockerTasks, ...), generating CI, or authoring/refactoring a zuke.ts build file. For first-time project scaffolding, use the zuke-setup skill instead.From its SKILL.md

Install
npx -y skills add zuke-build/zuke --skill zuke-write-build

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

16.9 KB, as published. Nobody here has run it

Write or edit a Zuke build

A build is a class that extends Build. Each target is a class field created with target() and made runnable with await run(MyBuild) at the bottom of zuke.ts (no import.meta.main guard — run no-ops on import).

<!-- check -->
import { Build, run, target } from "jsr:@zuke/core";
import { DenoTasks } from "jsr:@zuke/deno";

class CI extends Build {
  lint = target()
    .description("Lint sources")
    .executes(async () => {
      await DenoTasks.lint();
    });

  test = target()
    .description("Type-check and test")
    .dependsOn(this.lint)
    .executes(async () => {
      await DenoTasks.test((s) => s.allowAll().coverage("cov_profile"));
    });

  // A field named `default` runs when no target is named on the CLI.
  default = target().dependsOn(this.test).executes(() => {});
}

await run(CI);

Non-negotiable rules

  1. Dependencies are this.<field> references, never strings. .dependsOn(this.lint), not .dependsOn("lint") — so renames and typos are compile-time errors.
  2. A target may only depend on siblings declared above it. Class fields initialise top-to-bottom; a forward reference is undefined and is reported as an error (TypeScript also flags it, TS2729). Order fields so dependencies come first.
  3. Check the package catalogue before writing any command. llms.txt's ## Packages catalogue (raw: https://raw.githubusercontent.com/zuke-build/zuke/master/llms.txt) and the package table in references/cheatsheet.md are the only ways to answer "does a @zuke/<tool> wrapper exist for this CLI?" — per-package deno doc jsr:@zuke/<pkg> only describes a package whose name you already know; it cannot tell you a wrapper exists. Whatever runs in an .executes(...) body drives an external tool through its namespaced *Tasks object, configured with a settings lambda that mirrors the real CLI's flags — DenoTasks, NpmTasks, DockerTasks, GitTasks, and 30+ more — never a raw Deno.Command or shell string. jsr:@zuke/cmd (CmdTasks.exec) or the $ shell from jsr:@zuke/core/shell is the last resort, reached for only once the catalogue confirms no typed wrapper exists — using it for a tool that has a @zuke/<tool> package is a bug, not a style choice: it discards typed flags, argv purity, and tool resolution. (If a build delegates its side effects to your own tested modules behind injected clients, the wrapper rule still governs whatever those modules run in the target body.)
  4. A body is required, unless the target is one of the four forms that replace it: a service(), a .forEach() fan-out, a .waitsFor() gate, or a target declaring only .effect(...). Otherwise set .executes(...); it may be sync or async, and its return value is ignored — .executes(() => DenoTasks.lint()) is fine as-is; never wrap a single wrapper call in an async block just to discard its result.

Find the exact signature first

Before calling any task or settings method, confirm the real shape — but first confirm a wrapper exists at all: deno doc needs a package name to target, so it cannot answer "does one exist for this tool?"; only the catalogue (llms.txt's ## Packages list or the cheatsheet table below) can.

  • One package — prefer this in a consumer repo, once you know its name: deno doc jsr:@zuke/<package> (e.g. deno doc jsr:@zuke/deno). It resolves the version the project actually has installed, so it cannot describe an API that version lacks.
  • Whole surface: llms-full.txt (index: llms.txt) — at the repo root in the Zuke repo itself. From a consumer repo, fetch https://raw.githubusercontent.com/zuke-build/zuke/master/llms-full.txt (index: https://raw.githubusercontent.com/zuke-build/zuke/master/llms.txt). Both track master, so they can document symbols that are merged but not yet in any published release. Use them for breadth — which packages and tasks exist — and confirm a signature with deno doc before relying on it.
  • A quick map of the most common methods and task objects is in references/cheatsheet.md next to this file — read it when wiring targets, then verify specifics against the sources above.

Workflow for a change

  1. Read the existing zuke.ts to learn the targets already declared and their order.
  2. Identify the tool you need and look up its *Tasks object and settings methods (cheatsheet → deno doc / llms-full.txt).
  3. Add or edit the target field. Place it below every target it depends on. Wire dependencies with this.<field>.
  4. Validate: ./zuke --list shows it; ./zuke <target> --dry-run previews the plan; ./zuke <target> runs it.

Common building blocks (see the cheatsheet for details)

  • Parallel batches: group() + .partOf(this.group) run members concurrently; depend on the group to wait for all of them.
  • Reusable bundles: a component is a function returning related targets; assign it to a field and reference members as this.release.publish.
  • Long-lived processes: service() models a process that must stay running while dependents execute (dev server, database, mock API). Declared and depended on like a target, but with a .start(...) / .readyWhen(...) lifecycle instead of .executes(...); the executor starts it, waits until ready, then stops it in a finally so it never leaks. See the cheatsheet.
  • Target context & cancellation: a body may take a context — .executes((ctx) => …) — with ctx.runId, ctx.target, ctx.signal (an AbortSignal fired when the run is cancelled; a plain $`…` in the body is SIGTERM'd automatically), ctx.state, and ctx.dryRun. Zero-argument bodies keep working unchanged. Cancel a run programmatically by passing { signal } to execute. See the cheatsheet.
  • Caching: .inputs(...) / .outputs(...) make a target incremental. Add a remote store to share results across machines (fresh CI, teammates); --affected runs only targets changed since a git base; --no-cache / --no-remote-cache bypass them. A restore is confined to the target's declared .outputs(...) (and never .git/.zuke); a refused archive is a cache miss with a warning, not a failure. A cancelled run keeps its cache unless a compensation actually rolled something back.
  • Durable run state: persist a run's status and per-target metadata to a pluggable StateStore so it survives the process — turn it on with --state, ZUKE_STATE_DIR / ZUKE_STATE_URL, or override stateStore(). Every ZUKE_*_URL backend must be https: (loopback exempt; ZUKE_ALLOW_INSECURE_URL=1 opts out). In a body, ctx.state.set({ … }) / ctx.state.get() records per-target metadata (JSON, never secrets — secret parameters and redacted values are excluded). Inspect persisted runs afterwards with zuke runs list (filter by --status/--target/--since/--limit) and zuke runs show <id> (--json on both). Prune old ones with zuke runs prune --keep <age> --keep-last <n> (only terminal runs; never suspended/running). A run whose process is killed is picked up by zuke resume --check, which reaps it — its lease tells a dead holder from a slow one — and resumes it in the same sweep. A process that merely looked dead and then finds its lease taken over stops, running no compensations and settling nothing: the run is the new holder's now. override deadline() gives a run a wall-clock budget ("45m", or milliseconds) that survives suspension; an abandoned run found past it is settled failed with its compensations instead of resumed. On a shared store, set ZUKE_BUILD_ID (or rely on GITHUB_REPOSITORY) so each build only recovers its own runs — a resume runs this build's bodies against whatever record it is given, and a templated zuke.ts looks identical to the shape checks. See the cheatsheet.
  • Cross-run locks: .lock((s) => s.lockKey(...).withTtl("4h")) — a settings lambda — gives a target an exclusive claim across runs/machines; a second run wanting the same key fails with a LockConflictError naming the holder, or queues when the target adds .waitUpTo("30m") (paced by .pollEvery). The lambda runs after params resolve, so the key can read this.<param>.value. The lock releases when the target settles and expires after the TTL if the holder is killed. Needs a state store (a build with .lock() enables the filesystem store by default). See the cheatsheet.
  • External-event waits: .waitsFor((s) => s.on(externalSignal("approved")).timeout("72h")) makes a target a gate with no body: the run proceeds past it only when the trigger is satisfied, otherwise it suspends (state saved, exits 0) to be resumed later in a fresh process. Triggers: externalSignal(name) (payload read via ctx.signals) and resumeWhen(predicate). Continue it with zuke resume <id> --signal <name> [--data <json>] (or --check for predicate waits/timeouts) — exactly-once, re-running only the not-yet-succeeded targets. Needs a state store. See the cheatsheet / docs/orchestration.md.
  • Cancellation & compensation: .onCancel(() => this.rollback) registers a compensation that runs iff this target succeeded when the run is later cancelled — compensations run in reverse order, and the compensation body's ctx.state exposes the original target's persisted metadata (so a rollback reads what the deploy recorded). Cancel with zuke cancel <id> (or Ctrl-C, or the MCP cancel_run tool). Idempotent; a timed-out wait can route its onTimeout here ("cancel-run" or a named target). Needs a state store. See docs/orchestration.md.
  • Durable side effects: .effect(name, fn) records the intent to run fn before it runs, so a resume re-drives an effect a dead process left owed. Effects run after the body, in declaration order; a target may declare effects and no body. The guarantee is at-least-once, so write bodies that tolerate a repeat (an upsert, not an append), and read what the effect acts on from ctx.state rather than looking up "the current value" — a re-drive happens later, against a world that moved on. Needs a state store (enabled automatically). See the cheatsheet / docs/orchestration.md.
  • Fan-out over a list: .forEach(() => this.repos.value, (repo) => ({ checks: target()…, deploy: target()… }), (s) => s.concurrency(3).continueOnItemFailure()) runs the same pipeline over a runtime list — items concurrent, each item's stages sequential. Sub-targets are materialised at run time (parent[item].stage), each a first-class row in the summary and the run record; continueOnItemFailure() isolates a failed item. An .onCancel(...) on a fan-out stage runs per item on cancel (item-scoped ctx.state, reverse order). See docs/orchestration.md.
  • Typed inputs: parameter("...") (with .number() / .boolean() / .options(...) / .secret() / .required()), read as this.x.value, gated with .requires(this.x). .array() composes and comes last: .options(...).array() validates each element, .number().array() → number[], and a required list is .required().array() (required before array — .array().required() does not typecheck).
  • Secrets from a manager: parameter(...).secret().from(source) sources a value at run time (e.g. execSecret(...) shelling out to a secret CLI) and redacts it from all of Zuke's output. See the cheatsheet.
  • Provisioning tools: ToolTasks.install((s) => …) / toolchain((t) => …) fetch pinned, checksum-verified release binaries so a build is hermetic, and t.npm({ name, version, bin? }) / ToolTasks.npm(...) provision a version-pinned, cached npm-registry package (needs npm on PATH); hand the returned path to a wrapper's .toolPath(...). In a Node monorepo, resolve a wrapper's binary from node_modules/.bin npx-style instead — .fromNodeModules() on the settings (or ZUKE_TOOL_RESOLUTION=node_modules repo-wide) walks up for the local shim and falls back to PATH; .fromPath() forces PATH and an explicit .toolPath(...) always wins. See the cheatsheet / docs/tools.md.
  • Code-first CI: cicd({ provider: "github" }) generates and verifies the workflow YAML from the build.
  • Operate the build from an agent: zuke mcp serves the build over MCP so an AI client can list, inspect, and (with --allow-run) run targets — on stdio, or over HTTP with --http <host:port> (loopback by default; a non-loopback bind needs a ZUKE_MCP_TOKEN bearer token). With a state store it also exposes list_runs/show_run (+ signal_run, resume_check and cancel_run). Tier access with --allow-run=<globs> (an allow-list over invocation — invoking a target runs its dependencies, and the read tools narrow to the allow-listed targets' closure), --protect <globs> + ZUKE_OPERATOR_TOKEN (enforced over a run's whole plan, so a protected target reached as a dependency still needs the token), and --confirm-destructive; mark inspect-only targets .readOnly(). Mutating/denied calls are audited — read the trail on the host with zuke runs show mcp-audit; it is deliberately not readable over MCP. A registry-backed server (zuke register then zuke mcp --registry) instead serves every registered pipeline live, each as a run:<buildId>:<target> tool that takes the build's declared parameters (secrets excluded, validated, forwarded to the spawn) — see the cheatsheet. Because the registry names where a build launches from, a descriptor with a remote entry module is refused unless its origin is in ZUKE_REGISTRY_LAUNCH_HOSTS; zuke register writes a local module, so this only affects a hand-authored or second-party entry. For a shared, multi-user endpoint, override mcpIdentity() resolves a trusted caller per request from an authenticating proxy's header (it overrides the client-reported actor and flows to the audit trail, run records, and lock holders; a throwing hook rejects the request).
  • AI review & self-healing (@zuke/ai): gate a target on a structured LLM review of the diff (securityReviewer(...) etc. via .validateBefore), or attach aiFixer(...) with .recoverWith(...) so a failing target is diagnosed and (opt-in) auto-fixed, with a committable PR suggestion. Override recoverWith() on the build to apply one fixer to every target. A reviewer can go deeper and hold a discussion: .conventionsFile("AGENTS.md") (judged against the project's rules, read from the diff base), .fileContext() (whole changed files, not bare hunks), .verify() (adversarial re-check of every finding), and .discussion() (maintainers refute a finding by replying with its id — or, with .discussion((d) => d.threads()), by replying in the finding's own line-anchored review thread; accepted dismissals persist instead of resurfacing, including when the model rewords the finding — only platform-verified maintainer comments ever reach the model, on GitHub, GitLab, Azure DevOps and Bitbucket alike). See the cheatsheet's AI section.
  • Wait on an external GitHub workflow (@zuke/gh): in a .waitsFor(...) gate, s.on(githubWorkflow((g) => g.repo("o/r").workflow("e2e.yml"))) dispatches a workflow in another repo and suspends until it finishes; read the per-job result with readWorkflowResult(ctx.stateOf("<gate>")). Correlates by a run-name: marker by default, or .correlate("created-window") for a workflow you can't modify; fails fast (.discoveryTimeout(...)) if the run never correlates. The dispatched workflow has a contract: declare the marker input (zuke_marker, or rename via .markerInput(...)), echo it as its entire run-name: (equality, not substring), and receive any of its required: true inputs via .inputs(...) — see the cheatsheet's receiving-workflow contract. Triggers are extensible — write your own against the exported WaitTrigger/WaitContext.
  • OpenTelemetry export (@zuke/otel): register otel((s) => s.endpoint(…)) as a plugin (run(MyBuild, { plugins: [otel(…)] })) to ship run/target spans and zuke.run.started / zuke.run.suspended / zuke.runs counters as OTLP/HTTP JSON. Needs a state store; the trace id is derived from the run id, so a suspend/resume across processes is one trace. Config falls back to the standard OTEL_* env vars, and it is inert with no endpoint. Dependency-free.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.