How to write, change and check Gardener tasks (.gardener/tasks/*/TASK.md), the AI maintenance tasks this repository runs from GitHub Actions. Use before creating or editing anything under .gardener/.
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Gardener runs AI maintenance tasks on this repository from GitHub Actions. Each task is one Markdown file: YAML frontmatter says when it runs, what the model may read, and exactly which changes it may make; the body is the model's instructions. A planning job runs the model with only the declared tools and lets it plan only the declared effects. A separate job then applies that plan. Nothing outside the declaration is possible, whatever the instructions say.
.gardener/
gardener.json project settings (release pin, optional "handle"); edit only "handle"
gardener.lock.json generated; never edit
SKILL.md this file; generated
tasks/<dir>/TASK.md one task per directory; the only files you write
.github/workflows/
gardener-<id>.yml generated, one per task; never edit
gardener-sync.yml generated (or the "syncWorkflow" name in gardener.json); never edit.gardener/tasks/<dir>/TASK.md. Start from the template below or an existing task.npx @scuffi/gardener@0.1.22 generate from the repository root (or the repository's own
package.json script, if it has one). Use this exact version: a different one produces files the
pull request's Check tasks check rejects.generate prints and read its warnings. Repeat until it succeeds.TASK.md, .gardener/gardener.lock.json and the .github/workflows/gardener-*.yml
changes together. When they reach the default branch, the sync workflow enables the task.To change a task, edit its TASK.md and run generate again. To remove one, delete its
directory and run generate. Never hand-edit generated files.
---
schema: gardener.task/v1
id: bug-intake
name: Bug intake
description: Asks issue authors for missing reproduction details.
trigger:
event: github.issue.opened
labels-all: [bug]
tools:
- repository.list_files
- repository.read_file
effects:
- issue.comment.create
network:
default: deny
allow: []
deny: []
limits:
runtime-seconds: 300
max-turns: 12
max-tool-calls: 16
input-tokens: 60000
output-tokens: 16000
---
Read the issue and the code it mentions. If it is missing reproduction steps, expected behaviour or
a version, comment once (`issue.comment.create`) asking for exactly what is missing. Otherwise
change nothing. Then finish.Unknown keys are errors.
| Key | Required | Meaning |
|---|---|---|
schema | yes | Always gardener.task/v1. |
id | yes | Stable identity: lowercase letters, digits, ., _, -. Names the workflow gardener-<id>.yml. Unique per repository. |
name | yes | Short human name (up to 100 characters). |
description | yes | One or two sentences (up to 1,000 characters). |
trigger / triggers | exactly one | A single trigger object, or a list of them. See Triggers. |
tools | yes | What the model may read or run. At least one. See Tools. |
effects | no | What the task may change. Omit or [] for a read-only task. See Effects. |
network | yes | Must be exactly as in Network. |
limits | yes | Run budget. See Limits. |
model | no | Model ID. Defaults to "@cf/zai-org/glm-5.3". Quote IDs starting with @. |
draft | no | true makes the task run only by hand. See Trying a task. |
checkout | no | pull-request-head checks out a pull request's head instead of GitHub's merge preview, also for comments on a pull request and manual runs naming one. Needs a pull request or issue comment trigger. Implied by commit.merge_base, and when commit.create may write beyond gardener/**. |
reactions | no | false stops the 👀 reaction while the task works on an issue or comment, and the 🚀 or 😕 it leaves after. On by default. |
effect-scope | no | trigger confines every run to the issue or pull request that triggered it, even a maintainer's. See Who a run may act on. |
Each trigger is event: plus optional filters. A task may use each event at most once.
| Event | Filters |
|---|---|
github.issue.opened, github.issue.edited | labels-all, mentions, authors |
github.issue.labeled, .unlabeled, .reopened | labels-all, opened-by |
github.issue_comment.created, .edited | labels-all, mentions, authors, opened-by |
github.pull_request.opened, .edited | labels-all, mentions, authors |
github.pull_request.reopened, .synchronize, .ready_for_review, .converted_to_draft, .labeled, .unlabeled | labels-all, opened-by |
github.pull_request_review.submitted | labels-all, mentions, authors, opened-by |
github.pull_request_review_comment.created, .edited | labels-all, mentions, authors, opened-by |
github.discussion.created, .edited | labels-all, mentions, authors |
github.discussion.answered, .unanswered, .labeled, .unlabeled | labels-all, opened-by |
github.discussion_comment.created, .edited | labels-all, mentions, authors, opened-by |
github.push | branches (required), e.g. [main, 'release/*'] |
github.schedule | cron (required), five fields, e.g. 0 3 * * 1 |
github.workflow_dispatch | none |
labels-all: the issue, pull request or discussion must carry all of these labels. On the
.labeled triggers the label just added must also be one of them, so a later unrelated label
does not start the task again: one listed label runs exactly once, N listed labels up to N times.mentions: the text must @mention one of these handles. self means the handle in
.gardener/gardener.json, and generate fails if no handle is set.authors: who wrote the triggering text: maintainers (owner, members, collaborators, anyone
with write access), any, or a list of logins that may include maintainers, such as
[maintainers, "devin-ai-integration[bot]"]. It defaults to maintainers on comment and
review triggers and on any trigger with mentions, and to any elsewhere. Only set any with
mentions if the task is safe for anyone on the internet to start.opened-by: exact logins of who opened the issue, pull request or discussion, such as
["dependabot[bot]"], or ["github-actions[bot]"] for pull requests Gardener opened.[bot] logins inside a bracketed YAML list.| Tool | Gives the model |
|---|---|
repository.list_files | List files in the checkout. |
repository.read_file | Read files in the checkout. |
provider.api.read | Read-only GitHub API: REST GET/HEAD and GraphQL queries (labels, other issues, pull request diffs, check runs, ...). |
repository.exec | Run shell commands in the checkout (edit files, run tests). Unrestricted network access: do not add it unless the user explicitly asks, and say so. |
repository.merge_base | Write the merge of the pull request's base branch into the checkout, conflicts marked, for commit.merge_base. See Updating open pull requests. |
The event that triggered the run (issue, pull request, comment, ...) is always given to the model. Declare only the tools the instructions need.
effects is the complete list of changes the task may make, and they are applied automatically
with no human approval. Declare the narrowest exact kinds the instructions need. If the
instructions ask for an effect that isn't declared, it can't happen.
| Family | Kinds |
|---|---|
| Issues | issue.comment.create, issue.comment.update, issue.label.add, issue.label.remove, issue.assignee.add, issue.assignee.remove, issue.close, issue.reopen, issue.create |
| Pull requests | pull_request.comment.create, pull_request.comment.update, pull_request.review.submit, pull_request.reviewer.request, pull_request.reviewer.remove, pull_request.update, pull_request.label.add, pull_request.label.remove, pull_request.update_branch, pull_request.review_comment.reply, pull_request.review_thread.resolve, pull_request.open, pull_request.open_draft, pull_request.merge |
| Git | branch.create, commit.create, commit.merge_base |
| Discussions | discussion.comment.create, discussion.comment.update, discussion.answer.mark, discussion.answer.unmark, discussion.close, discussion.reopen |
| Checks | check.rerun |
| Releases | release.create, release.update, release.publish, release.delete |
Family globs (issue.*, pull_request.*, git.*, discussion.*, check.*, release.*) grant
every kind in the family, including pull_request.merge and release.delete. Prefer exact kinds.
git.* includes commit.merge_base only for tasks that declare the repository.merge_base tool.
Labels must already exist in the repository; the model can't create them. Tell it to pick only
from existing labels (with provider.api.read it can list them).
pull_request.open / open_draft with labels also needs pull_request.label.add.
pull_request.review_comment.reply (a reply in a review thread, by any comment id in it) and
pull_request.review_thread.resolve (by the thread's GraphQL node id) act only on the pull
request that triggered the run. Resolve only threads the task's own commit fixed.
Code changes need repository.exec (to edit files), branch.create, commit.create and
pull_request.open_draft (or .open). The commit is made from files changed in the checkout,
on a branch created from the checked-out commit. Changes under .github/workflows/,
.github/actions/, .gardener/, .git/, CODEOWNERS and .github/dependabot.yml are refused.
Branch-writing kinds (branch.create, commit.create, commit.merge_base, pull_request.open,
pull_request.open_draft) may only use gardener/** branches. To allow others, write the kind as
an entry. The list replaces the default, and patterns never match the default branch unless it is
named exactly:
effects:
- kind: commit.create
branches: ["gardener/**", "docs/*"]Opening pull requests also needs Settings → Actions → General → Allow GitHub Actions to create and approve pull requests turned on in the repository.
The model plans every step before any runs. A later step can use an earlier step's output (for example a new pull request's URL in a comment); the model is told how.
message.send posts one short message about an issue, pull request or discussion to the chat
space in the GARDENER_CHAT_WEBHOOK repository secret (a Google Chat or Slack incoming webhook),
after every other step applied. Name it exactly (message.* is not a glob). At most one per run;
the summary is one line, at most 400 characters, without <, >, & or backticks. It can't be
combined with repository.exec. Tell the model when to post, for example only for urgent issues.
The message reads 🐛 repo #123 (@author) - Title, then the summary; the emoji follows the
subject's labels (regression, bug, enhancement, question, documentation, else 🆕).
Nothing is sent if the subject changed since the model read it. On Slack, a rerun after a lost
answer can post twice; Google Chat never posts a plan's message twice.
task.run starts another task of this repository on the run's issue or pull request, or on a pull
request the plan opens, after every other step applied: - task.run: { tasks: [fix] }. List
every task it may start; the model picks one and the subject, nothing else. At most one per run,
always the last step, never retried. The started task inherits the first run's trust and counts against
the first author's runs-per-author; chains stop at depth 3. A task that only runs this way
declares - event: github.workflow_dispatch with subject: issue or subject: pull_request.
The task's job gets actions: write. generate refuses unknown targets, the task itself, and
cycles.
A task can bring a pull request's base branch into it with a merge commit, for example when a maintainer comments "@handle fix the conflicts" on the pull request.
github.issue_comment.created (with mentions and the default authors: maintainers). The run then checks out the pull request's head; forks and closed pull requests
plan on the default branch instead.repository.merge_base tool, repository.exec, and commit.merge_base with the
pull request branches it may push to (branches:, as above; never the default branch).repository.merge_base; edit only the conflicted files it lists to resolve
them; run the repository's checks; then merge with one commit.merge_base using the values the tool
returned. Never run git merge, rebase, commit or push: a moved head or a changed index
fails the run. If the conflicts can't be resolved, explain why in a comment instead.commit.merge_base acts only on the pull request that triggered the run and never rewrites
history. Protected paths may change only to exactly what the base branch has.Must be one of these, exactly. Host lists must be empty.
network: # tasks without repository.exec
default: deny
allow: []
deny: []network: # tasks with repository.exec
default: allow
allow: []
deny: []| Key | Meaning | Allowed |
|---|---|---|
runtime-seconds | Wall-clock limit for the run. | 30–21,000 |
max-turns | Model responses. | at least 3 |
max-tool-calls | Tool calls (each file read, API call or command counts, plus the final finish_task, so the task's own work gets one fewer). | at least 3 |
input-tokens | Largest single model request. The whole conversation is resent each turn. | any positive |
output-tokens | Total generated across the run, including reasoning. | at least 16 × max-turns |
max-effect-operations | Optional cap on the changes one run may make. Needs at least one effect. | 1–1,000 |
max-effect-bytes | Optional cap on the plan's size. | 1,024–50,000,000 |
runs | Optional cap on this task's runs across everyone, maintainers included, e.g. 50/day. | <count>/<minute|hour|day|week|month> |
runs-per-author | Cap on runs started by each non-maintainer. Defaults to 3/day; unlimited turns it off. | as runs, or unlimited |
A run that runs out of any budget fails with budget-exceeded and changes nothing. A run over runs or
runs-per-author completes as a skip without calling the model. A month is 30 days.
Runs started by a maintainer, a schedule, a manual run or a person's push may act anywhere the task's
effects allow. Runs started by anyone else may only act on the issue or pull request that triggered
them, or on what the run itself creates; they can't merge or approve a pull request they opened,
change releases or re-run checks. "Anyone else" includes every bot (a bot's push too) and logins named
in authors who aren't maintainers. Mentioning other issues in text is always fine.
What Gardener's starter tasks use:
| Task | runtime-seconds | max-turns | max-tool-calls | input-tokens | output-tokens |
|---|---|---|---|---|---|
| Triage a new issue (comment and labels) | 300 | 12 | 16 | 80000 | 16000 |
| Reply to a mention | 480 | 12 | 16 | 128000 | 16000 |
| Review a pull request | 480 | 16 | 24 | 128000 | 16000 |
The default model reasons before it answers, which spends output-tokens; below about 16000 a run
can be cut off before its first tool call. Each file read, API call, edit or test run is at least
one turn and one tool call, so a task that changes and tests code needs several times these
budgets.
The body is the model's prompt. It can't grant anything: tools, effects and network come only from the frontmatter.
issue.comment.create), and say when to change nothing. Write it as direct instructions
("Comment with…", "Open a pull request…"): the model acts through those effects, and the text it
writes is published as its reply.Set draft: true, generate and merge. The task then runs only by hand, from its workflow in the
Actions tab or with gh workflow run gardener-<id>.yml -f issue=<n> (or -f pull_request=<n>,
-f prompt=...). Trigger filters don't apply to manual runs. A draft task's effects are still
applied for real. Remove draft: true and generate again to make it live.
cloudflare/agents
5c33d89
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.