{"slug":"zuke-write-build","title":"zuke-write-build","summary":"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 buil","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-08-25T15:08:01.233638Z","repo":{"url":"https://github.com/zuke-build/zuke","stars":45,"forks":1,"license":"MIT","updatedAt":"2026-09-28T08:31:55Z"},"bodyHtml":"<hr>\n<h2>name: zuke-write-build\ndescription: 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.</h2>\n<h1>Write or edit a Zuke build</h1>\n<p>A build is a class that <strong>extends <code>Build</code></strong>. Each <strong>target is a class field</strong>\ncreated with <code>target()</code> and made runnable with <code>await run(MyBuild)</code> at the\nbottom of <code>zuke.ts</code> (no <code>import.meta.main</code> guard — <code>run</code> no-ops on import).</p>\n\n<pre><code>import { Build, run, target } from \"jsr:@zuke/core\";\nimport { DenoTasks } from \"jsr:@zuke/deno\";\n\nclass CI extends Build {\n  lint = target()\n    .description(\"Lint sources\")\n    .executes(async () =&gt; {\n      await DenoTasks.lint();\n    });\n\n  test = target()\n    .description(\"Type-check and test\")\n    .dependsOn(this.lint)\n    .executes(async () =&gt; {\n      await DenoTasks.test((s) =&gt; s.allowAll().coverage(\"cov_profile\"));\n    });\n\n  // A field named `default` runs when no target is named on the CLI.\n  default = target().dependsOn(this.test).executes(() =&gt; {});\n}\n\nawait run(CI);\n</code></pre>\n<h2>Non-negotiable rules</h2>\n<ol>\n<li><strong>Dependencies are <code>this.&lt;field&gt;</code> references, never strings.</strong>\n<code>.dependsOn(this.lint)</code>, not <code>.dependsOn(\"lint\")</code> — so renames and typos are\ncompile-time errors.</li>\n<li><strong>A target may only depend on siblings declared <em>above</em> it.</strong> Class fields\ninitialise top-to-bottom; a forward reference is <code>undefined</code> and is reported\nas an error (TypeScript also flags it, <code>TS2729</code>). Order fields so\ndependencies come first.</li>\n<li><strong>Check the package catalogue before writing any command.</strong> <code>llms.txt</code>'s\n<code>## Packages</code> catalogue (raw:\n<a href=\"https://raw.githubusercontent.com/zuke-build/zuke/master/llms.txt\">https://raw.githubusercontent.com/zuke-build/zuke/master/llms.txt</a>) and the\npackage table in <a href=\"references/cheatsheet.md\"><code>references/cheatsheet.md</code></a> are\nthe only ways to answer \"does a <code>@zuke/&lt;tool&gt;</code> wrapper exist for this CLI?\" —\nper-package <code>deno doc jsr:@zuke/&lt;pkg&gt;</code> only describes a package whose name\nyou already know; it cannot tell you a wrapper exists. Whatever runs in an\n<code>.executes(...)</code> body drives an external tool through its namespaced <code>*Tasks</code>\nobject, configured with a <strong>settings lambda</strong> that mirrors the real CLI's\nflags — <code>DenoTasks</code>, <code>NpmTasks</code>, <code>DockerTasks</code>, <code>GitTasks</code>, and 30+ more —\nnever a raw <code>Deno.Command</code> or shell string. <code>jsr:@zuke/cmd</code> (<code>CmdTasks.exec</code>)\nor the <code>$</code> shell from <code>jsr:@zuke/core/shell</code> is the <strong>last resort</strong>, reached\nfor only once the catalogue confirms no typed wrapper exists — using it for a\ntool that has a <code>@zuke/&lt;tool&gt;</code> package is a <strong>bug</strong>, not a style choice: it\ndiscards typed flags, argv purity, and tool resolution. (If a build delegates\nits side effects to your own tested modules behind injected clients, the\nwrapper rule still governs whatever those modules run in the target body.)</li>\n<li><strong>A body is required</strong>, unless the target is one of the four forms that\nreplace it: a <code>service()</code>, a <code>.forEach()</code> fan-out, a <code>.waitsFor()</code> gate, or a\ntarget declaring only <code>.effect(...)</code>. Otherwise set <code>.executes(...)</code>; it may\nbe sync or async, and its return value is ignored —\n<code>.executes(() =&gt; DenoTasks.lint())</code> is fine as-is; never wrap a single\nwrapper call in an <code>async</code> block just to discard its result.</li>\n</ol>\n<h2>Find the exact signature first</h2>\n<p>Before calling any task or settings method, confirm the real shape — but first\nconfirm a wrapper exists at all: <code>deno doc</code> needs a package name to target, so\nit cannot answer \"does one exist for this tool?\"; only the catalogue\n(<code>llms.txt</code>'s <code>## Packages</code> list or the cheatsheet table below) can.</p>\n<ul>\n<li><strong>One package — prefer this in a consumer repo, once you know its name:</strong>\n<code>deno doc jsr:@zuke/&lt;package&gt;</code> (e.g. <code>deno doc jsr:@zuke/deno</code>). It resolves\nthe version the project actually has installed, so it cannot describe an API\nthat version lacks.</li>\n<li><strong>Whole surface:</strong> <code>llms-full.txt</code> (index: <code>llms.txt</code>) — at the repo root in\nthe Zuke repo itself. From a consumer repo, fetch\n<a href=\"https://raw.githubusercontent.com/zuke-build/zuke/master/llms-full.txt\">https://raw.githubusercontent.com/zuke-build/zuke/master/llms-full.txt</a>\n(index: <a href=\"https://raw.githubusercontent.com/zuke-build/zuke/master/llms.txt\">https://raw.githubusercontent.com/zuke-build/zuke/master/llms.txt</a>).\nBoth track <code>master</code>, so they can document symbols that are merged but not yet\nin any published release. Use them for breadth — which packages and tasks\nexist — and confirm a signature with <code>deno doc</code> before relying on it.</li>\n<li>A quick map of the most common methods and task objects is in\n<a href=\"references/cheatsheet.md\"><code>references/cheatsheet.md</code></a> next to this file —\nread it when wiring targets, then verify specifics against the sources above.</li>\n</ul>\n<h2>Workflow for a change</h2>\n<ol>\n<li>Read the existing <code>zuke.ts</code> to learn the targets already declared and their\norder.</li>\n<li>Identify the tool you need and look up its <code>*Tasks</code> object and settings\nmethods (cheatsheet → <code>deno doc</code> / <code>llms-full.txt</code>).</li>\n<li>Add or edit the target field. Place it <strong>below</strong> every target it depends on.\nWire dependencies with <code>this.&lt;field&gt;</code>.</li>\n<li>Validate: <code>./zuke --list</code> shows it; <code>./zuke &lt;target&gt; --dry-run</code> previews the\nplan; <code>./zuke &lt;target&gt;</code> runs it.</li>\n</ol>\n<h2>Common building blocks (see the cheatsheet for details)</h2>\n<ul>\n<li><strong>Parallel batches:</strong> <code>group()</code> + <code>.partOf(this.group)</code> run members\nconcurrently; depend on the group to wait for all of them.</li>\n<li><strong>Reusable bundles:</strong> a <em>component</em> is a function returning related targets;\nassign it to a field and reference members as <code>this.release.publish</code>.</li>\n<li><strong>Long-lived processes:</strong> <code>service()</code> models a process that must stay <em>running\nwhile dependents execute</em> (dev server, database, mock API). Declared and\ndepended on like a target, but with a <code>.start(...)</code> / <code>.readyWhen(...)</code>\nlifecycle instead of <code>.executes(...)</code>; the executor starts it, waits until\nready, then stops it in a <code>finally</code> so it never leaks. See the cheatsheet.</li>\n<li><strong>Target context &amp; cancellation:</strong> a body may take a context —\n<code>.executes((ctx) =&gt; …)</code> — with <code>ctx.runId</code>, <code>ctx.target</code>, <code>ctx.signal</code> (an\n<code>AbortSignal</code> fired when the run is cancelled; a plain <code>$`…`</code> in the body\nis <code>SIGTERM</code>'d automatically), <code>ctx.state</code>, and <code>ctx.dryRun</code>. Zero-argument\nbodies keep working unchanged. Cancel a run programmatically by passing\n<code>{ signal }</code> to <code>execute</code>. See the cheatsheet.</li>\n<li><strong>Caching:</strong> <code>.inputs(...)</code> / <code>.outputs(...)</code> make a target incremental. Add a\n<strong>remote store</strong> to share results across machines (fresh CI, teammates);\n<code>--affected</code> runs only targets changed since a git base; <code>--no-cache</code> /\n<code>--no-remote-cache</code> bypass them. A restore is confined to the target's\ndeclared <code>.outputs(...)</code> (and never <code>.git</code>/<code>.zuke</code>); a refused archive is a\ncache miss with a warning, not a failure. A cancelled run keeps its cache\nunless a compensation actually rolled something back.</li>\n<li><strong>Durable run state:</strong> persist a run's status and per-target metadata to a\npluggable <code>StateStore</code> so it survives the process — turn it on with <code>--state</code>,\n<code>ZUKE_STATE_DIR</code> / <code>ZUKE_STATE_URL</code>, or <code>override stateStore()</code>. Every\n<code>ZUKE_*_URL</code> backend must be <code>https:</code> (loopback exempt;\n<code>ZUKE_ALLOW_INSECURE_URL=1</code> opts out). In a body, <code>ctx.state.set({ … })</code> /\n<code>ctx.state.get()</code> records per-target metadata (JSON, <strong>never secrets</strong> —\nsecret parameters and redacted values are excluded). Inspect persisted runs\nafterwards with <code>zuke runs list</code> (filter by\n<code>--status</code>/<code>--target</code>/<code>--since</code>/<code>--limit</code>) and <code>zuke runs show &lt;id&gt;</code> (<code>--json</code>\non both). Prune old ones with <code>zuke runs prune --keep &lt;age&gt; --keep-last &lt;n&gt;</code>\n(only terminal runs; never suspended/running). A run whose process is killed\nis picked up by <code>zuke resume --check</code>, which reaps it — its lease tells a dead\nholder from a slow one — and resumes it in the same sweep. A process that\nmerely <em>looked</em> dead and then finds its lease taken over <strong>stops</strong>, running no\ncompensations and settling nothing: the run is the new holder's now.\n<code>override deadline()</code> gives a run a wall-clock budget (<code>\"45m\"</code>, or\nmilliseconds) that survives suspension; an abandoned run found past it is\nsettled <code>failed</code> with its compensations instead of resumed. On a <strong>shared</strong>\nstore, set <code>ZUKE_BUILD_ID</code> (or rely on <code>GITHUB_REPOSITORY</code>) so each build only\nrecovers its own runs — a resume runs <em>this</em> build's bodies against whatever\nrecord it is given, and a templated <code>zuke.ts</code> looks identical to the shape\nchecks. See the cheatsheet.</li>\n<li><strong>Cross-run locks:</strong> <code>.lock((s) =&gt; s.lockKey(...).withTtl(\"4h\"))</code> — a settings\nlambda — gives a target an exclusive claim across runs/machines; a second run\nwanting the same key fails with a <code>LockConflictError</code> naming the holder, or\nqueues when the target adds <code>.waitUpTo(\"30m\")</code> (paced by <code>.pollEvery</code>). The\nlambda runs after params resolve, so the key can read <code>this.&lt;param&gt;.value</code>.\nThe lock releases when the target settles and expires after the TTL if the\nholder is killed. Needs a state store (a build with <code>.lock()</code> enables the\nfilesystem store by default). See the cheatsheet.</li>\n<li><strong>External-event waits:</strong>\n<code>.waitsFor((s) =&gt; s.on(externalSignal(\"approved\")).timeout(\"72h\"))</code> makes a\ntarget a <strong>gate</strong> with no body: the run proceeds past it only when the trigger\nis satisfied, otherwise it <strong>suspends</strong> (state saved, exits 0) to be resumed\nlater in a fresh process. Triggers: <code>externalSignal(name)</code> (payload read via\n<code>ctx.signals</code>) and <code>resumeWhen(predicate)</code>. Continue it with\n<code>zuke resume &lt;id&gt; --signal &lt;name&gt; [--data &lt;json&gt;]</code> (or <code>--check</code> for predicate\nwaits/timeouts) — exactly-once, re-running only the not-yet-succeeded targets.\nNeeds a state store. See the cheatsheet / <code>docs/orchestration.md</code>.</li>\n<li><strong>Cancellation &amp; compensation:</strong> <code>.onCancel(() =&gt; this.rollback)</code> registers a\ncompensation that runs <strong>iff this target succeeded</strong> when the run is later\ncancelled — compensations run in reverse order, and the compensation body's\n<code>ctx.state</code> exposes the original target's persisted metadata (so a rollback\nreads what the deploy recorded). Cancel with <code>zuke cancel &lt;id&gt;</code> (or Ctrl-C, or\nthe MCP <code>cancel_run</code> tool). Idempotent; a timed-out wait can route its\n<code>onTimeout</code> here (<code>\"cancel-run\"</code> or a named target). Needs a state store. See\n<code>docs/orchestration.md</code>.</li>\n<li><strong>Durable side effects:</strong> <code>.effect(name, fn)</code> records the intent to run <code>fn</code>\nbefore it runs, so a resume re-drives an effect a dead process left owed.\nEffects run after the body, in declaration order; a target may declare effects\nand no body. The guarantee is <strong>at-least-once</strong>, so write bodies that tolerate\na repeat (an upsert, not an append), and read what the effect acts on from\n<code>ctx.state</code> rather than looking up \"the current value\" — a re-drive happens\nlater, against a world that moved on. Needs a state store (enabled\nautomatically). See the cheatsheet / <code>docs/orchestration.md</code>.</li>\n<li><strong>Fan-out over a list:</strong>\n<code>.forEach(() =&gt; this.repos.value, (repo) =&gt; ({ checks: target()…, deploy: target()… }), (s) =&gt; s.concurrency(3).continueOnItemFailure())</code>\nruns the same pipeline over a runtime list — items concurrent, each item's\nstages sequential. Sub-targets are materialised at run time\n(<code>parent[item].stage</code>), each a first-class row in the summary and the run\nrecord; <code>continueOnItemFailure()</code> isolates a failed item. An <code>.onCancel(...)</code>\non a fan-out stage runs per item on cancel (item-scoped <code>ctx.state</code>, reverse\norder). See <code>docs/orchestration.md</code>.</li>\n<li><strong>Typed inputs:</strong> <code>parameter(\"...\")</code> (with <code>.number()</code> / <code>.boolean()</code> /\n<code>.options(...)</code> / <code>.secret()</code> / <code>.required()</code>), read as <code>this.x.value</code>, gated\nwith <code>.requires(this.x)</code>. <code>.array()</code> composes and comes <strong>last</strong>:\n<code>.options(...).array()</code> validates each element, <code>.number().array()</code> →\n<code>number[]</code>, and a required list is <code>.required().array()</code> (required before\narray — <code>.array().required()</code> does not typecheck).</li>\n<li><strong>Secrets from a manager:</strong> <code>parameter(...).secret().from(source)</code> sources a\nvalue at run time (e.g. <code>execSecret(...)</code> shelling out to a secret CLI) and\n<strong>redacts</strong> it from all of Zuke's output. See the cheatsheet.</li>\n<li><strong>Provisioning tools:</strong> <code>ToolTasks.install((s) =&gt; …)</code> / <code>toolchain((t) =&gt; …)</code>\nfetch pinned, checksum-verified release binaries so a build is hermetic, and\n<code>t.npm({ name, version, bin? })</code> / <code>ToolTasks.npm(...)</code> provision a\nversion-pinned, cached npm-registry package (needs <code>npm</code> on <code>PATH</code>); hand the\nreturned path to a wrapper's <code>.toolPath(...)</code>. In a Node monorepo, resolve a\nwrapper's binary from <code>node_modules/.bin</code> npx-style instead —\n<code>.fromNodeModules()</code> on the settings (or <code>ZUKE_TOOL_RESOLUTION=node_modules</code>\nrepo-wide) walks up for the local shim and falls back to PATH; <code>.fromPath()</code>\nforces PATH and an explicit <code>.toolPath(...)</code> always wins. See the cheatsheet /\n<code>docs/tools.md</code>.</li>\n<li><strong>Code-first CI:</strong> <code>cicd({ provider: \"github\" })</code> generates and verifies the\nworkflow YAML from the build.</li>\n<li><strong>Operate the build from an agent:</strong> <code>zuke mcp</code> serves the build over MCP so\nan AI client can list, inspect, and (with <code>--allow-run</code>) run targets — on\nstdio, or over HTTP with <code>--http &lt;host:port&gt;</code> (loopback by default; a\nnon-loopback bind needs a <code>ZUKE_MCP_TOKEN</code> bearer token). With a state store\nit also exposes <code>list_runs</code>/<code>show_run</code> (+ <code>signal_run</code>, <code>resume_check</code> and\n<code>cancel_run</code>). Tier access with <code>--allow-run=&lt;globs&gt;</code> (an allow-list over\n<strong>invocation</strong> — invoking a target runs its dependencies, and the read tools\nnarrow to the allow-listed targets' closure), <code>--protect &lt;globs&gt;</code> +\n<code>ZUKE_OPERATOR_TOKEN</code> (enforced over a run's <strong>whole plan</strong>, so a protected\ntarget reached as a dependency still needs the token), and\n<code>--confirm-destructive</code>; mark inspect-only targets <code>.readOnly()</code>.\nMutating/denied calls are audited — read the trail on the host with\n<code>zuke runs show mcp-audit</code>; it is deliberately not readable over MCP. A\n<strong>registry-backed</strong> server (<code>zuke register</code> then <code>zuke mcp --registry</code>)\ninstead serves every registered pipeline live, each as a\n<code>run:&lt;buildId&gt;:&lt;target&gt;</code> tool that takes the build's declared parameters\n(secrets excluded, validated, forwarded to the spawn) — see the cheatsheet.\nBecause the registry names <em>where</em> a build launches from, a descriptor with a\n<strong>remote</strong> entry module is refused unless its origin is in\n<code>ZUKE_REGISTRY_LAUNCH_HOSTS</code>; <code>zuke register</code> writes a local module, so this\nonly affects a hand-authored or second-party entry. For a shared, multi-user\nendpoint, <code>override mcpIdentity()</code> resolves a <strong>trusted</strong> caller per request\nfrom an authenticating proxy's header (it overrides the client-reported actor\nand flows to the audit trail, run records, and lock holders; a throwing hook\nrejects the request).</li>\n<li><strong>AI review &amp; self-healing (<code>@zuke/ai</code>):</strong> gate a target on a structured LLM\nreview of the diff (<code>securityReviewer(...)</code> etc. via <code>.validateBefore</code>), or\nattach <code>aiFixer(...)</code> with <code>.recoverWith(...)</code> so a failing target is\ndiagnosed and (opt-in) auto-fixed, with a committable PR suggestion. Override\n<code>recoverWith()</code> on the build to apply one fixer to every target. A reviewer\ncan go deeper and hold a discussion: <code>.conventionsFile(\"AGENTS.md\")</code> (judged\nagainst the project's rules, read from the diff base), <code>.fileContext()</code> (whole\nchanged files, not bare hunks), <code>.verify()</code> (adversarial re-check of every\nfinding), and <code>.discussion()</code> (maintainers refute a finding by replying with\nits id — or, with <code>.discussion((d) =&gt; d.threads())</code>, by replying in the\nfinding's own line-anchored review thread; accepted dismissals persist instead\nof resurfacing, including when the model rewords the finding — only\nplatform-verified maintainer comments ever reach the model, on GitHub, GitLab,\nAzure DevOps and Bitbucket alike). See the cheatsheet's AI section.</li>\n<li><strong>Wait on an external GitHub workflow (<code>@zuke/gh</code>):</strong> in a <code>.waitsFor(...)</code>\ngate, <code>s.on(githubWorkflow((g) =&gt; g.repo(\"o/r\").workflow(\"e2e.yml\")))</code>\ndispatches a workflow in another repo and suspends until it finishes; read the\nper-job result with <code>readWorkflowResult(ctx.stateOf(\"&lt;gate&gt;\"))</code>. Correlates by\na <code>run-name:</code> marker by default, or <code>.correlate(\"created-window\")</code> for a\nworkflow you can't modify; fails fast (<code>.discoveryTimeout(...)</code>) if the run\nnever correlates. The <strong>dispatched</strong> workflow has a contract: declare the\nmarker input (<code>zuke_marker</code>, or rename via <code>.markerInput(...)</code>), echo it as\nits <em>entire</em> <code>run-name:</code> (equality, not substring), and receive any of its\n<code>required: true</code> inputs via <code>.inputs(...)</code> — see the cheatsheet's\nreceiving-workflow contract. Triggers are extensible — write your own against\nthe exported <code>WaitTrigger</code>/<code>WaitContext</code>.</li>\n<li><strong>OpenTelemetry export (<code>@zuke/otel</code>):</strong> register <code>otel((s) =&gt; s.endpoint(…))</code>\nas a plugin (<code>run(MyBuild, { plugins: [otel(…)] })</code>) to ship run/target spans\nand <code>zuke.run.started</code> / <code>zuke.run.suspended</code> / <code>zuke.runs</code> counters as\nOTLP/HTTP JSON. Needs a state store; the trace id is derived from the run id,\nso a suspend/resume across processes is one trace. Config falls back to the\nstandard <code>OTEL_*</code> env vars, and it is inert with no endpoint. Dependency-free.</li>\n</ul>\n","files":[{"path":"references/cheatsheet.md","sizeBytes":108295,"isText":true},{"path":"SKILL.md","sizeBytes":22288,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"notes-only","suspicious":0,"notes":5,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-29T23:41:13.214455Z","sha256":"827946BE2ED0743C3918B1012A0A36D589A0FEE1661920D6C6E1880BFF874B92","sizeBytes":45589},"review":null,"source":{"repositoryUrl":"https://github.com/zuke-build/zuke","path":"plugins/zuke/skills/zuke-write-build","license":"MIT","commit":"e721ea91eba3334cd6f2be3a52d91cb7e1450ec1","subtreeSha":"BEFBEEADD28B7A1BE740900EE7A160CDD17BF70F31F04436968BB8FC545A6B48","lastSyncedAt":"2026-09-29T23:32:59.588969Z"},"reviewedAt":"2026-09-29T23:41:15.227873Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/zuke-build/zuke/tree/master/plugins/zuke/skills/zuke-write-build"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install zuke-build-zuke@llmmart"},{"target":"git","command":"git clone https://github.com/zuke-build/zuke.git"}]}