Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
writing-issues
robot-council/cli
claude-code
github-issues
issue-templates
acceptance-criteria
markdown
house-style
agent-briefs

GitHub issue conventions for this repository: imperative verb-first titles (or symptom-first for bugs) with aggressive inline-code markup and no trailing period; a 1–3-sentence lede that often opens with a provenance line (`Follow-up to #N.`, `Spun off from #N.`, `Part of #N.`); ordered section inventory (`## Why` / `## Background`, `## Current state` / `## Current behavior` / `## The bug` / `## Root cause` / `## Reproduction`, `## What we're building` / `## Scope` / `## Approach` / `## Proposed fix`, mandatory `## Acceptance criteria` GitHub task-list of observable outcomes ending in a test item, `## Out of scope`, `## References`, `## Notes`); archetypes for bug / feature / research-spike / follow-up-cleanup issues; the duplicate check (including pull requests, and reading the match total before the rows); labels; the mutually-exclusive `afk`/`hitl` execution-mode convention flipped as forks are decided, with `epic` containers exempt; epics and sub-issues; dependencies expressed ONLY as native GitHub dependency edges and never restated in the body; cross-repo `owner/repo#N` refs; linked absolute-branch file paths; and the standing rule to NEVER add a "generated by AI" disclaimer. Activate whenever drafting, filing, rewriting, or critiquing the title or body of a GitHub issue for this repository.

Install
php artisan boost:add-skill robot-council/cli

Save this content to: .claude/skills/writing-issues/SKILL.md

# Writing Issues

This skill captures the house style for GitHub issue titles and bodies. **It is one shared
document, carried identically by `robot-council/core` and `robot-council/cli`, so it names no
repository of its own** -- the recipes below take `{owner}` and `{repo}` from the checkout, and the
duplicate check reads the repository once into `$here`. Where a repository is named in this file it
is because a measurement was taken there, which is a fact rather than an example.

An issue is a brief for whoever picks it up — often an autonomous
agent. Write it so the work is unambiguous before anyone touches code: state the *why*, pin
down the *current* behavior, describe the *desired* outcome, and bound the scope explicitly.
The defining feature of a good issue here is a crisp, testable **Acceptance criteria**
checklist.

This is the sibling of [`writing-pull-requests`](../writing-pull-requests/SKILL.md) and
[`writing-commits`](../writing-commits/SKILL.md); the inline-code, file-link, bullet, and
em-dash conventions are shared — when in doubt, match those skills.

## Start from the archetype template

[`.github/ISSUE_TEMPLATE/`](../../../.github/ISSUE_TEMPLATE) holds a Markdown skeleton per
archetype — `1-bug.md`, `2-feature.md`, `3-research-spike.md`, `4-follow-up-cleanup.md`. A human
filing through the web UI gets one pre-filled; **you should start from the matching file too**,
so both paths produce the same shape. Copy it, fill it in, strip the front matter and the
`<!-- … -->` guidance, and pass the result as the body file, then apply the labels its front
matter names (a body file does not carry them). Where an issue doesn't fit an archetype, build
the body from the section inventory below.

**This skill is the source of truth; the templates carry only the skeleton and a pointer back
here.** When the house style changes, change it here first and reconcile the templates — never
the reverse, or the two silently diverge.

## Output format

When proposing an issue, give the **title** on its own line and present the **body as a single
fenced Markdown code block** so it can be pasted. If the body contains fenced blocks, open the
outer fence with more backticks than any inner one. The body is GitHub-Flavored Markdown — never
include an `H1` (`#`) heading; GitHub renders the title separately. Top-level sections are `##`,
sub-sections `###`.

When you create the issue yourself, **write the body to a file and pass it as a file from the
start** — never inline the Markdown as an argument. Issue bodies are dense with backticks, `$`,
`!`, backslashes, and fenced code blocks, every one of which the shell mangles or errors on as an
inline argument (the failure is intermittent, so an inline body looks fine until one finally trips
it). The REST form keeps working when the GraphQL quota behind `gh issue` is spent (see
[`github-api-budget`](../../rules/github-api-budget.md)):

**`{owner}` and `{repo}` are literal.** `gh` fills them from the checkout, so these act on the
repository you are in rather than one named here -- which matters because most of them are
**writes**. Keep the quotes: PowerShell parses an unquoted `{…}` as a script block and splits the
argument. The mechanism and its bounds are in
[`writing-pull-requests`](../writing-pull-requests/SKILL.md).

```bash
gh api -X POST 'repos/{owner}/{repo}/issues' -f title='…' -F [email protected] -f 'labels[]=<existing-label>'
gh api -X PATCH 'repos/{owner}/{repo}/issues/<n>' -F [email protected]
```

`gh issue create --body-file body.md` and `gh issue edit <n> --body-file body.md` are the
equivalent wrapper forms.

## The disclaimer rule (non-negotiable)

**Never** add a `> *This was generated by AI during triage.*` blockquote (or any equivalent
"written/generated by AI" line) to an issue body, and strip one when rewriting an issue.

## Titles

The title is an imperative, verb-first phrase naming the deliverable or the outcome — what the
issue *accomplishes*, not a topic label. No trailing period. Keep it specific and scannable.
The examples below are illustrative.

- **Default form — imperative outcome:** `Publish the council migration under a timestamped filename`,
  `Validate `robot-council.members` entries on package boot`, `Add a `--dry-run` flag to the `robot-council` command`.
- **Bugs may lead with the symptom** instead of an imperative, naming the faulty component and
  the user-visible consequence: `RobotCouncil::members() returns null (not []) when no members are configured`.
- **Disambiguating parenthetical** — append the mechanism in parens when it sharpens the title:
  `(not [])`, `(string "1" vs boolean)`.
- **Inline code in titles** — backtick anything a developer would type or grep: commands
  (`robot-council`), config keys (`robot-council.members`), classes and methods.
- **No Conventional-Commit prefixes.** Never prefix an issue title with `feat:` / `fix:` /
  `chore:` etc. — those belong on commits, not issues. Strip the prefix when rewriting and fold
  the verb into the imperative phrase (`fix: members null` → `Fix RobotCouncil::members() returning null`).
- **Research spike:** prefix with `Research:` — `Research: decide how council members are persisted`.

## Body: the standard skeleton

Most issues follow this spine. Drop the sections that don't apply; never drop **Acceptance
criteria**.

1. **Lede** — 1–3 sentences. Frame the problem and the desired outcome. Lead with the
   user-visible effect or root cause, not the implementation. The lede frequently opens with a
   **provenance line**: `Follow-up to #N.`, `Spun off from #N.`, `Part of #N.`,
   `Discovered while … for #N.` A provenance line says where the issue *came from*; it never
   states a dependency — see *Dependencies belong in the edges*.
2. **Context** — `## Why` (motivation, often the strongest section on a feature) and/or the
   current-state sections below.
3. **Desired outcome / plan** — `## What we're building`, `## Scope`, `## Approach`,
   `## Proposed fix`.
4. **`## Acceptance criteria`** — mandatory. A GitHub task-list of observable outcomes.
5. **`## Out of scope`** — explicit non-goals, each pointing at its tracking issue.
6. **`## References`** — links, file paths, sibling tickets.
7. **No relationship line.** Dependencies live in GitHub's native dependency edges
   (`blocked_by` / `blocking`) and are **never** restated in the body.

## Section inventory

Use the section that fits; these are the established names (don't invent synonyms casually):

- **`## Why`** — why this matters now. On features this carries the weight: explain the forces
  that make the work necessary.
- **`## Background`** — domain context the reader needs before the rest makes sense.
- **`## Current state`** / **`## Current behavior`** — what exists today, with file, class, and
  config-key references. The factual baseline the work changes.
- **`## The bug`** / **`## Root cause`** / **`## Reproduction`** — bug issues only. `## The
  bug` / `## Root cause` walk the mechanism (quote the offending code in a fenced block);
  `## Reproduction` gives the concrete trigger and the observed error.
- **`## Desired behavior`** — the target contract, stated as behavior not implementation.
- **`## What we're building`** / **`## Target behavior`** — the deliverable for features (tables
  are welcome for concrete mappings).
- **`## Scope`** — a bounded list of the changes the issue covers (common on follow-up
  cleanups).
- **`## Approach`** / **`## Proposed approach`** / **`## Chosen approach`** / **`## Proposed
  fix`** / **`## Fix`** — the recommended path. Name the design decision and why; include
  before/after code where it removes ambiguity. For a fix already made, `## Fix` describes what
  was done.
- **`## Questions to resolve`** — research spikes: the numbered open questions the spike must
  answer.
- **`## Acceptance criteria`** — see below. Always present.
- **`## Out of scope`** — first-class. Each deferred item links the issue that owns it (or says
  "file a follow-up").
- **`## References`** — bullet list: source files (linked), related tickets, external docs/URLs.
- **`## Notes`** — caveats, edge cases, "worth a follow-up" asides.
- **`## Test plan`** / **`## Files`** — optional, used when the issue doubles as the working
  record of an in-flight change (mirrors the PR conventions; `[x]` for already-done).

## Acceptance criteria — the core of every issue

This is the section that makes an issue actionable. Conventions:

- A GitHub **task list** (`- [ ]`), one box per independently verifiable outcome. Use `- [x]`
  only when the issue records work already completed.
- Each item is an **observable outcome**, not an implementation step. "Running `robot-council`
  with no members configured exits `0` and prints the empty-council message" — not "add a
  default to the config lookup".
- Be concrete: name the command, the config key, the class, the output, the exception that must
  no longer throw.
- **Bound the behavior on both sides** — assert the new behavior *and* assert the unaffected
  cases still hold ("an absent key yields `[]`; a configured list is returned unchanged").
- **End with a test/coverage item.** Nearly every issue closes its AC list with a regression or
  feature-test requirement ("a Pest test covers the absent-key and configured-list cases").
- For research spikes, the AC are **decisions documented**, not code ("a persistence approach is
  chosen and recorded with its trade-offs", "a clear go / no-go recommendation is recorded").

## Relationships and cross-references

### Dependencies belong in the edges, never in the body

**Never write `Blocked by #N.` or `Blocks #N` into an issue body.** Express a dependency
*only* as a native GitHub dependency edge (`dependencies/blocked_by`, `dependencies/blocking`);
the REST calls are in [`github-api-budget`](../../rules/github-api-budget.md).

**Why.** Prose and edges drift apart, and the prose is the half that fails silently — the edge
is machine-maintained, the sentence is not. The recurring shapes: a body naming a **closed**
ticket as its gate while the real edge points elsewhere; a list of blockers half of which have
shipped; a claim that a blocker in another repository "has no edge to hang it on", which is not
true, because edges work across repositories. In each, the edge was right and the sentence was
wrong.

There is a second, sharper failure the prose form invites: **a bare `#N` resolves against the
current repository**, so a reference copied from another repository's tracker silently points at
an unrelated issue here. An edge stores a repository-qualified object id and cannot make that
mistake.

**The carve-out — context cross-references stay.** What is banned is the *dependency claim*,
not the reference. These remain welcome, because the graph does not track them and they cannot
fall out of sync with it:

- **Provenance** — `Follow-up to #N.`, `Spun off from #N.`, `Part of #N.`
- **Inline cross-refs** — `tracked in #N`, `tracked separately in #N`, `see #N / #M`.
- **Naming what something is** — "the contract this codes against is `#N`".

Where a body needs to explain *why* something waits, describe the **condition** — what has to be
true, what artifact must exist — rather than naming a ticket as the gate.

**If the prose is right and the graph is not, add the missing edge.** Do not keep the sentence
as a substitute for the edge it is describing.

### Reference forms

- **Cross-repo refs** — `owner/repo#N`, either bare or as a backticked link where it reads better
  (`` [`owner/repo#N`](https://github.com/owner/repo/issues/N) ``). Qualify **every** cross-repo
  reference: a bare `#N` silently resolves against the repository the body lives in.
- **In `## Out of scope` / `## References` bullets** — end the bullet with `— tracked in #N.`
  so the residual work visibly has a home.

## Inline code, links, bullets

Shared with `writing-pull-requests` — the short version:

- **Backtick aggressively**: file paths, class/method names, config keys
  (`robot-council.members`), commands (`vendor/bin/pest`), env vars, version arrows
  (`` `13.31.0` → `13.32.0` ``).
- **File paths are linked.** The target is the **absolute branch URL**
  (`` [`<path>`](https://github.com/<owner>/<repo>/blob/<branch>/<path>) ``,
  `/tree/` for directories), exactly as the PR skill specifies; vendor refs may include line
  anchors (`…/PackageServiceProvider.php#L20-L35`).
- **Fenced code blocks** (`php`, `yaml`, `bash`) quote the offending or proposed code in bug
  issues, and config excerpts elsewhere. Show before/after as two labelled blocks or
  inline-commented code, not a `diff` block.
- **Bullets are `-` only**, nested with a 2-space indent. **Bold-tag lead-ins**
  (`- **Per-key default** — …`) categorize list items. **Em-dash `—`** separates a tag from its
  prose. **Don't force-wrap** prose.
- **Tables** for naturally tabular data — config-key inventories, version matrices.

## Labels

Apply the labels that match the issue's nature; multiple are normal. **The label set is curated
by hand, so re-list it before labeling rather than trusting the summary below:**

```bash
gh api 'repos/{owner}/{repo}/labels?per_page=100' --jq '.[] | "\(.name)\t\(.description)"'
```

Apply only labels that exist, choosing by each label's description. If an issue needs a label
that does not exist, name it in your proposal and ask before creating it; never create labels as
a side effect of filing. There is **no `bug` label**: a bug is `development` with a symptom-first
title and, where available, the `Bug` issue type.

- **`development`** — code work in the package; the default, carried by most issues.
- **`build`** — Composer, PHPStan, Pint, Pest configuration, GitHub Actions.
- **`dependencies`** — applied by Dependabot to its update pull requests; not for hand-filed issues.
- **`documentation`** — README and other prose.
- **`security`** — security-sensitive work. An exploitable vulnerability does not go in a public
  issue at all: report it privately per [`security-audit`](../security-audit/SKILL.md).
- **`ui`** — the package's published views or other user-visible output.
- **`research`** — a spike whose deliverable is a documented decision, not code.
- **`QA`** — verify expected behavior, usually paired with the build ticket it validates.
- **`upstream`** — a report or request to file against a dependency, per
  [`filing-defects-across-repos`](../../rules/filing-defects-across-repos.md).
- **`test-flake`** — an intermittent failure that passes in isolation; not a real defect.
- **`browser-tests`** — browser-driven test work (the Pest browser plugin is not installed yet).
- **`quiet-machine`** — timing or measurement work that needs no concurrent runs, per
  [`measurement-parity`](../../rules/measurement-parity.md).
- **`macos-environment`** · **`windows-environment`** — must be completed on that platform.
- **`handed-back`** — pull requests only: returned to the author by
  [`pre-merge-check`](../../rules/pre-merge-check.md) with a finding outstanding.
- **`afk`** · **`hitl`** · **`epic`** — execution mode, below. A `hitl` issue also carries its
  flavor: **`decision-fork`** (a design decision) or **`action-flavor`** (a real-world action).

### Priority — `fleet-facing` / `housekeeping` (exactly one)

Every open issue also carries **exactly one** of these, and it decides the order work is taken in: **`fleet-facing` comes first, `housekeeping` last.**

- **`fleet-facing`** — changes what an agent or operator on the fleet experiences: the service's API and behavior, the bridge and its follower, enrollment and credential storage, the tool list a harness sees, the messages a session reads, and the documented steps a fleet user follows. A measurement that decides whether one of those works on a platform counts too.
- **`housekeeping`** — repository upkeep with no direct effect on the fleet: CI wiring, the release-notes generator, skills and rules, a test flake, or a measurement of code that has already been replaced.

**Classify by who notices if it never ships.** If only people working in the repository would, it is `housekeeping`, even when it changes shipped code or carries `security`. A cleanup of the service's API is `fleet-facing`, because the deployed bridge talks to it. An `epic` carries the label its slices mostly share. Still file `housekeeping` residue per [`closing-a-ticket`](../../rules/closing-a-ticket.md); the label lowers its priority, not its right to a ticket.

### Issue types

The `robot-council` organization enables the `Task`, `Bug`, and `Feature` issue types. Set exactly
one on every issue: `Bug` for a bug, `Feature` for a feature, and `Task` for a research spike, a
decision fork, a follow-up cleanup, or an epic. The templates declare the matching `type:` for
issues filed through the web UI. When filing through REST, pass the type's name:

```bash
gh api -X POST 'repos/{owner}/{repo}/issues' -f title='…' -F [email protected] -f type=Feature
```

**Read the type back after setting it.** GitHub's REST description says a type set without push
access is silently dropped, so the call succeeds either way:
`gh api 'repos/{owner}/{repo}/issues/<n>' --jq '.type.name'`.

### Execution mode — `afk` / `hitl` (exactly one — except on an `epic`)

Every open issue carries **exactly one** of `afk` / `hitl`. It tells whoever triages the tracker what *kind* of attention the issue needs to be *completed*: an autonomous coding agent, or a human.

**The exception is `epic`.** An issue labeled `epic` is exempt: it is a container whose slices each carry their own execution mode, not a unit of work anyone picks up and builds. An epic with neither label is **correctly labeled** — don't "fix" it. Apply `hitl` to an epic only where the container *itself* waits on a human.

- **`afk`** (away-from-keyboard) — buildable autonomously: the design is **pinned or already decided**, or the fix is **fully specified**, so there is **no genuine design fork to resolve in the issue's own scope**. Most build slices, dependency bumps, refactors, and already-specified bug fixes are `afk`. The test: *could a competent agent finish this alone, without a human deciding anything or doing anything outside the repo?* If yes, `afk`.
- **`hitl`** (human-in-the-loop) — completing it **requires a human**, for either reason: a human **decision** — an unresolved design fork (the issue carries a `## Questions to resolve` / `## Decisions to resolve` section), a policy / cost / go-no-go call, or a settled decision being re-opened; or a real-world **action** — a step outside the repo a coding agent can't perform (provisioning or credentialing, creating an external account, installing an app, publishing somewhere that needs a human's login). If a human must **decide or act** before the issue can close, `hitl`. There are only these two modes; fold real-world actions into `hitl`.

For the **decision** flavor, surface the fork the right way — lead with a compare/contrast packet, *then* the interactive prompt — following the [`design-decision-forks`](../../rules/design-decision-forks.md) rule (don't restate it here).

**A decision-fork ticket delivers two things and nothing else: the decision, recorded, and the follow-up tickets it produces.** Build work the decision unblocks goes into those follow-ups, blocked by the fork through native dependency edges. Its acceptance criteria describe *a decision being recorded and its follow-ups existing*, never a build being done — a criterion naming a file to change belongs on a follow-up.

**How to classify** — a **leaf** issue takes the mode of what *its own* completion needs; don't inherit a blocker's mode (blockers carry their own labels). An **`epic`** is exempt — classify its slices, not the container. When genuinely on the fence, prefer `hitl`: it's safer to route an ambiguous issue to a human than to let an agent build past an unmade decision.

**Keep the label current — flip it when the classification changes.** Flip `hitl` → `afk` once the fork is **decided and recorded** on the issue — relabel (dropping `decision-fork` too) and reconcile the body in the *same change* (retire the `## Questions to resolve` / `## Decisions to resolve` section, tick the decision-recorded acceptance criterion). What remains on a fork ticket after the flip is filing its follow-ups, not building them. Flip `afk` → `hitl` the moment work **surfaces a genuine design fork** an agent shouldn't decide alone — relabel (adding `decision-fork`), add a `## Decisions to resolve` section, and settle it with the user (per [`design-decision-forks`](../../rules/design-decision-forks.md)) before building.

## Epics and sub-issues

An **`epic`** is a container. It holds the shared context a body of work rests on and **carries no deliverables of its own** — no acceptance criteria, no "decisions taken here", nothing anyone could pick up and build. Every deliverable is a sub-issue. If a reader cannot tell what to *do* from a slice alone, the slice is under-written, not the epic.

That constraint is what makes an epic useful. A container with its own checklist competes with its slices, and the work ends up recorded in two places that drift.

**When a ticket becomes an epic.** Convert one when it has accumulated several independently shippable deliverables — usually visible as an acceptance-criteria list whose items would each make a reasonable ticket. Two shapes recur:

- A **decision plus its build.** Spin the decision into its own gating slice rather than leaving it at the head of the container, so the fork is something a human can be handed and the epic is not itself `hitl`.
- **Measurement, decision, then build.** File the measuring slices even when they are already done — see below.

**File retroactive sub-issues for work already finished.** When converting a ticket that has already produced results, give each completed piece its own sub-issue, record what it found in the body, and close it as `completed`. It costs a minute and it gives the finding a home: an epic body is a poor place for a measurement, because the next edit overwrites it.

**Labels on a container.** An `epic` takes the topic labels that describe its subject and **not** `afk` / `hitl`, nor labels that describe how its slices will be worked — those belong on the slices that earn them.

**Wiring, and it is not the same thing as a dependency.** A sub-issue says *this is part of that*; a `blocked_by` edge says *this cannot start until that finishes*. Most epics need both, and they are set independently:

```bash
id=$(gh api 'repos/{owner}/{repo}/issues/<child>' --jq '.id')     # numeric .id, NOT the issue number
gh api -X POST 'repos/{owner}/{repo}/issues/<parent>/sub_issues' -F sub_issue_id=$id
```

**Sub-issues work across repositories.** The endpoint keys on the global database id, so a slice living in another repository attaches to an epic here exactly like a local one; listing it only in prose loses it. Read the set back with `GET …/sub_issues`, and see [`github-api-budget`](../../rules/github-api-budget.md) for why these are REST calls.

## Check for an existing issue first

Before creating any issue, search the tracker for one that already covers the same **concept** —
match on the idea, not just the title, since an existing issue may be worded completely differently.
Run a few searches with varied terms and **include closed issues** (a fixed or won't-fix issue still
means *don't* refile):

```bash
# One block, because the control and the questions have to be the same instrument. The repository
# is resolved from the checkout once; the terms go in `-f` FIELDS rather than into the URL, so `gh`
# encodes them. `-X GET` because `gh api` sends POST as soon as a field is present, and `timeout`
# because the failure mode here is a hang rather than an error -- both explained below.
here=$(gh api 'repos/{owner}/{repo}' --jq .full_name)

for q in "robot-council" "<symptom or feature>" "<affected file / class / config key>" "<class of problem or label>"; do
  printf '%s -> ' "$q"
  timeout 25 gh api -X GET search/issues -f q="repo:$here $q" -f per_page=100 \
    --jq '"matches: \(.total_count) (showing \(.items|length))  from: \(.items[0].repository_url // "-" | sub(".*/repos/"; ""))",
          (.items[] | "  \(if .pull_request then "PR " else "iss" end) #\(.number)  \(.title)")' \
    || echo "NO ANSWER (rc=$?) -- not a clean check"
done

echo "checkout: $here   <- every from: above must equal this"
```

**A search term written into the URL with a space in it HANGS**, which is why the terms are passed
as fields. Measured on 2026-09-24 with `gh` 2.92.0: `gh api "search/issues?q=repo:…+two words"`
never returns. `--verbose` shows why -- it sends `GET /search/issues?q=…+two words&per_page=1
HTTP/1.1`, a request line with a raw space in it, and then waits for a reply that never comes. One
such call ran **22 minutes** before it was killed by hand. It is not the search endpoint's fault
and not `q`'s: the same raw space anywhere in a query string does it, measured with
`repos/{owner}/{repo}/issues?labels=a b`. A space in the **path** is harmless by contrast --
`repos/{owner}/{repo}/iss ues` answers `404` promptly. The hazard is that the placeholders above
are phrases, so following the recipe literally is what triggers it.

**`-f` costs the placeholders, and that is the reason `$here` exists.** `gh` substitutes `{owner}`
and `{repo}` in a **path**, and in a field whose value is *exactly* the placeholder -- which is what
`writing-pull-requests`'s GraphQL recipe relies on with `-F owner='{owner}'`. It does **not**
substitute them inside a longer field value: measured, `-f q='repo:{owner}/{repo} term'` sends the
braces literally and GitHub answers `Validation Failed`. So the repository is read once through the
path form and interpolated as text.

**Read the `matches:` line before the rows.** `search/issues` returns one page, so a query broader than the page size is answered with a silent prefix, and the truncated output is identical in shape to a query that genuinely found everything. In `UAMS-Web/uams-statamic` (measured 2026-09-06) a broad query reported hundreds of matches while a narrow one reported single digits, and nothing but the total distinguished the truncated page from the complete one.

**A total greater than the rows shown means the check has not answered the question.** Either paginate (add `-f page=2`, until a short page) or narrow the query until the total fits — "nothing matches" is not a conclusion you can draw from a truncated page. **`incomplete_results` is not that signal**: it reports a server-side timeout, not pagination, and it is `false` on truncated responses.

**`gh issue list` and `gh search issues` exclude pull requests; the REST search endpoint above does not.** That matters because a fix often ships as a pull request with no issue behind it, so the wrapper hides exactly the in-flight work the check exists to find — in `UAMS-Web/uams-statamic` (2026-09-05) a wrapper search returned the issue and never the pull request implementing it. The `.pull_request` key is what distinguishes the two kinds, hence the `PR`/`iss` prefix. The endpoint cannot phrase-match and it indexes comments; see [`github-api-budget`](../../rules/github-api-budget.md).

**Everything above guards against a query that is too NARROW. None of it guards against a query that did not RUN.** Those produce the same output, and the second one is the easier mistake: a malformed `--jq` expression prints its parse error to stderr, `gh` exits non-zero, and a pipeline ending in `|| echo "(none)"` prints exactly what a genuinely clean check prints. Observed while filing against `robot-council/cli` on 2026-09-21, where the check read:

```
=== duplicate check ===
  (none)
```

Two issues were filed on the strength of that line. Re-run afterwards with a working parser the check was genuinely clean — so no duplicate was filed, but that was luck rather than method, and the same output would have appeared had there been ten.

**So show the query finding something, in the same invocation, before reading it finding nothing.** A term certain to appear in the repository is enough, and it costs one call.

**A count is not enough on its own, and the control has to name the repository that answered.** A term common enough to be a reliable control is common enough to match in *either* repository, so a healthy total confirms the instrument while saying nothing about which tracker was asked — and a duplicate check aimed at the wrong one reads exactly like a clean one. Measured from a `robot-council/core` checkout on 2026-09-24: the term `robot-council` returned **197** matches aimed at this repository and **126** aimed at the other. Both look fine. `repository_url` off the first item is what tells them apart.

That is why `robot-council` is the **first** term in the block above rather than a separate
invocation: the control and the questions then share one endpoint, one `--jq`, one quoting and one
bound, so nothing can differ between them except the term.

Read the control's total and its `from:` first. If the total is `0`, if the line is missing entirely, or if `from:` is not this checkout, stop — nothing below it is evidence. This is [`an-empty-result-is-not-evidence`](../../rules/an-empty-result-is-not-evidence.md) applied to the duplicate check; that rule owns the general form and the reasoning, and is not restated here.

**`from:` reads `-` when a query matches nothing**, which is the honest answer and not a failure: there is no item to take a repository from. That is why the total is read first — the two lines answer different questions.

Vary the terms across the symptom, the affected file/class/config key, and the class of problem —
one query rarely surfaces a differently-worded duplicate. **The `repo:` qualifier above is `$here`,
read from the checkout**, so these search the repository you are in; for work that spans
repositories, substitute the other one for `$here` in a further query and expect its `from:` to name
that repository rather than this one. If anything plausibly covers the concept:
**link it and skip**, or add a comment / sharpen the existing issue — don't open a duplicate. Only
file once you've confirmed nothing matches.

## Archetypes at a glance

Pick the skeleton that matches the work:

- **Bug fix** → lede (with discovery provenance) · `## The bug` or `## Root cause` (offending
  code) · `## Reproduction` · `## Proposed fix` (fix code) · `## Acceptance criteria` (ends in a
  regression test) · `## References`.
- **Feature** → lede · `## Why` · `## What we're building` (+ behavior detail / tables) ·
  `## Acceptance criteria` · `## Out of scope` · `## References`.
- **Research spike** → `Research:`-prefixed title · lede ("**Research spike.** … deliverable is
  a documented decision, not code") · `## Why this is a spike` · `## Questions to resolve` ·
  `## Acceptance criteria` (decisions + go/no-go) · `## References`.
- **Follow-up cleanup** → `Follow-up to #N.` lede · `## Scope` · short `## Acceptance criteria`.
  `Follow-up to #N.` is provenance and stays; any dependency on `#N` is an edge.

## Tone and voice

- Technical, prose-driven, specific. Lead with *why*, then *what*. Concrete file paths, config
  keys, commands, URLs, error strings over generalities.
- Bound scope deliberately — `## Out of scope` is part of writing a good issue, not an
  afterthought.
- No exclamation marks. Use `*emphasis*` sparingly, only on the word that carries the sentence.
- **Impersonal voice throughout** — every artifact posts under one account, so no `I`/`my`/`you`/`your`, and no vouching stance. See [`impersonal-voice-in-github-artifacts`](../../rules/impersonal-voice-in-github-artifacts.md).
- **No emoji** — not in the title, the body, or any comment. Use words: a bolded clause says what a
  glyph was standing in for, and a glyph breaks downstream where nothing can report it. See
  [`no-emoji-in-durable-records`](../../rules/no-emoji-in-durable-records.md).

## Example shape

A compact bug issue demonstrating the core conventions. Illustrative only: `members()` and the
config key are invented to show the shape.

**Title:** `RobotCouncil::members() returns null (not []) when no members are configured`

`````markdown
````
Discovered while writing feature tests for the `robot-council` command. `RobotCouncil\RobotCouncil::members()` declares an `array` return type, but when `robot-council.members` is absent from config it returns `null`, so PHP throws a `TypeError` and the command fails before printing anything.

## Root cause

[`src/RobotCouncil.php`](https://github.com/robot-council/core/blob/main/src/RobotCouncil.php) -- **the package's file, named because the example is its**; the application has no `src/`:

```php
public function members(): array
{
    return config('robot-council.members');   // null when the key is absent
}
```

## Proposed fix

Default the lookup to an empty array so the signature holds:

```php
return config('robot-council.members', []);
```

## Acceptance criteria

- [ ] `RobotCouncil::members()` returns `[]` when `robot-council.members` is absent.
- [ ] `RobotCouncil::members()` still returns the configured list unchanged when the key is present.
- [ ] A Pest test covers the absent-key and configured-list cases.
````
`````

(Lede opens with discovery provenance and leads with the user-visible failure. `## Root cause`
quotes the offending code; `## Proposed fix` shows the one-line change. Acceptance criteria are
observable outcomes that bound both the fixed and the unaffected behavior and close with a
regression test. No AI disclaimer. File path links to the absolute branch URL.)

Writing Issues

This skill captures the house style for GitHub issue titles and bodies. It is one shared document, carried identically by robot-council/core and robot-council/cli, so it names no repository of its own -- the recipes below take {owner} and {repo} from the checkout, and the duplicate check reads the repository once into $here. Where a repository is named in this file it is because a measurement was taken there, which is a fact rather than an example.

An issue is a brief for whoever picks it up — often an autonomous agent. Write it so the work is unambiguous before anyone touches code: state the why, pin down the current behavior, describe the desired outcome, and bound the scope explicitly. The defining feature of a good issue here is a crisp, testable Acceptance criteria checklist.

This is the sibling of writing-pull-requests and writing-commits; the inline-code, file-link, bullet, and em-dash conventions are shared — when in doubt, match those skills.

Start from the archetype template

.github/ISSUE_TEMPLATE/ holds a Markdown skeleton per archetype — 1-bug.md, 2-feature.md, 3-research-spike.md, 4-follow-up-cleanup.md. A human filing through the web UI gets one pre-filled; you should start from the matching file too, so both paths produce the same shape. Copy it, fill it in, strip the front matter and the <!-- … --> guidance, and pass the result as the body file, then apply the labels its front matter names (a body file does not carry them). Where an issue doesn't fit an archetype, build the body from the section inventory below.

This skill is the source of truth; the templates carry only the skeleton and a pointer back here. When the house style changes, change it here first and reconcile the templates — never the reverse, or the two silently diverge.

Output format

When proposing an issue, give the title on its own line and present the body as a single fenced Markdown code block so it can be pasted. If the body contains fenced blocks, open the outer fence with more backticks than any inner one. The body is GitHub-Flavored Markdown — never include an H1 (#) heading; GitHub renders the title separately. Top-level sections are ##, sub-sections ###.

When you create the issue yourself, write the body to a file and pass it as a file from the start — never inline the Markdown as an argument. Issue bodies are dense with backticks, $, !, backslashes, and fenced code blocks, every one of which the shell mangles or errors on as an inline argument (the failure is intermittent, so an inline body looks fine until one finally trips it). The REST form keeps working when the GraphQL quota behind gh issue is spent (see github-api-budget):

{owner} and {repo} are literal. gh fills them from the checkout, so these act on the repository you are in rather than one named here -- which matters because most of them are writes. Keep the quotes: PowerShell parses an unquoted {…} as a script block and splits the argument. The mechanism and its bounds are in writing-pull-requests.

gh api -X POST 'repos/{owner}/{repo}/issues' -f title='…' -F [email protected] -f 'labels[]=<existing-label>'
gh api -X PATCH 'repos/{owner}/{repo}/issues/<n>' -F [email protected]

gh issue create --body-file body.md and gh issue edit <n> --body-file body.md are the equivalent wrapper forms.

The disclaimer rule (non-negotiable)

Never add a > *This was generated by AI during triage.* blockquote (or any equivalent "written/generated by AI" line) to an issue body, and strip one when rewriting an issue.

Titles

The title is an imperative, verb-first phrase naming the deliverable or the outcome — what the issue accomplishes, not a topic label. No trailing period. Keep it specific and scannable. The examples below are illustrative.

  • Default form — imperative outcome: Publish the council migration under a timestamped filename, Validate robot-council.members entries on package boot, Add a --dry-runflag to therobot-council command.
  • Bugs may lead with the symptom instead of an imperative, naming the faulty component and the user-visible consequence: RobotCouncil::members() returns null (not []) when no members are configured.
  • Disambiguating parenthetical — append the mechanism in parens when it sharpens the title: (not []), (string "1" vs boolean).
  • Inline code in titles — backtick anything a developer would type or grep: commands (robot-council), config keys (robot-council.members), classes and methods.
  • No Conventional-Commit prefixes. Never prefix an issue title with feat: / fix: / chore: etc. — those belong on commits, not issues. Strip the prefix when rewriting and fold the verb into the imperative phrase (fix: members null → Fix RobotCouncil::members() returning null).
  • Research spike: prefix with Research: — Research: decide how council members are persisted.

Body: the standard skeleton

Most issues follow this spine. Drop the sections that don't apply; never drop Acceptance criteria.

  1. Lede — 1–3 sentences. Frame the problem and the desired outcome. Lead with the user-visible effect or root cause, not the implementation. The lede frequently opens with a provenance line: Follow-up to #N., Spun off from #N., Part of #N., Discovered while … for #N. A provenance line says where the issue came from; it never states a dependency — see Dependencies belong in the edges.
  2. Context — ## Why (motivation, often the strongest section on a feature) and/or the current-state sections below.
  3. Desired outcome / plan — ## What we're building, ## Scope, ## Approach, ## Proposed fix.
  4. ## Acceptance criteria — mandatory. A GitHub task-list of observable outcomes.
  5. ## Out of scope — explicit non-goals, each pointing at its tracking issue.
  6. ## References — links, file paths, sibling tickets.
  7. No relationship line. Dependencies live in GitHub's native dependency edges (blocked_by / blocking) and are never restated in the body.

Section inventory

Use the section that fits; these are the established names (don't invent synonyms casually):

  • ## Why — why this matters now. On features this carries the weight: explain the forces that make the work necessary.
  • ## Background — domain context the reader needs before the rest makes sense.
  • ## Current state / ## Current behavior — what exists today, with file, class, and config-key references. The factual baseline the work changes.
  • ## The bug / ## Root cause / ## Reproduction — bug issues only. ## The bug / ## Root cause walk the mechanism (quote the offending code in a fenced block); ## Reproduction gives the concrete trigger and the observed error.
  • ## Desired behavior — the target contract, stated as behavior not implementation.
  • ## What we're building / ## Target behavior — the deliverable for features (tables are welcome for concrete mappings).
  • ## Scope — a bounded list of the changes the issue covers (common on follow-up cleanups).
  • ## Approach / ## Proposed approach / ## Chosen approach / ## Proposed fix / ## Fix — the recommended path. Name the design decision and why; include before/after code where it removes ambiguity. For a fix already made, ## Fix describes what was done.
  • ## Questions to resolve — research spikes: the numbered open questions the spike must answer.
  • ## Acceptance criteria — see below. Always present.
  • ## Out of scope — first-class. Each deferred item links the issue that owns it (or says "file a follow-up").
  • ## References — bullet list: source files (linked), related tickets, external docs/URLs.
  • ## Notes — caveats, edge cases, "worth a follow-up" asides.
  • ## Test plan / ## Files — optional, used when the issue doubles as the working record of an in-flight change (mirrors the PR conventions; [x] for already-done).

Acceptance criteria — the core of every issue

This is the section that makes an issue actionable. Conventions:

  • A GitHub task list (- [ ]), one box per independently verifiable outcome. Use - [x] only when the issue records work already completed.
  • Each item is an observable outcome, not an implementation step. "Running robot-council with no members configured exits 0 and prints the empty-council message" — not "add a default to the config lookup".
  • Be concrete: name the command, the config key, the class, the output, the exception that must no longer throw.
  • Bound the behavior on both sides — assert the new behavior and assert the unaffected cases still hold ("an absent key yields []; a configured list is returned unchanged").
  • End with a test/coverage item. Nearly every issue closes its AC list with a regression or feature-test requirement ("a Pest test covers the absent-key and configured-list cases").
  • For research spikes, the AC are decisions documented, not code ("a persistence approach is chosen and recorded with its trade-offs", "a clear go / no-go recommendation is recorded").

Relationships and cross-references

Dependencies belong in the edges, never in the body

Never write Blocked by #N. or Blocks #N into an issue body. Express a dependency only as a native GitHub dependency edge (dependencies/blocked_by, dependencies/blocking); the REST calls are in github-api-budget.

Why. Prose and edges drift apart, and the prose is the half that fails silently — the edge is machine-maintained, the sentence is not. The recurring shapes: a body naming a closed ticket as its gate while the real edge points elsewhere; a list of blockers half of which have shipped; a claim that a blocker in another repository "has no edge to hang it on", which is not true, because edges work across repositories. In each, the edge was right and the sentence was wrong.

There is a second, sharper failure the prose form invites: a bare #N resolves against the current repository, so a reference copied from another repository's tracker silently points at an unrelated issue here. An edge stores a repository-qualified object id and cannot make that mistake.

The carve-out — context cross-references stay. What is banned is the dependency claim, not the reference. These remain welcome, because the graph does not track them and they cannot fall out of sync with it:

  • Provenance — Follow-up to #N., Spun off from #N., Part of #N.
  • Inline cross-refs — tracked in #N, tracked separately in #N, see #N / #M.
  • Naming what something is — "the contract this codes against is #N".

Where a body needs to explain why something waits, describe the condition — what has to be true, what artifact must exist — rather than naming a ticket as the gate.

If the prose is right and the graph is not, add the missing edge. Do not keep the sentence as a substitute for the edge it is describing.

Reference forms

  • Cross-repo refs — owner/repo#N, either bare or as a backticked link where it reads better ([`owner/repo#N`](https://github.com/owner/repo/issues/N)). Qualify every cross-repo reference: a bare #N silently resolves against the repository the body lives in.
  • In ## Out of scope / ## References bullets — end the bullet with — tracked in #N. so the residual work visibly has a home.

Inline code, links, bullets

Shared with writing-pull-requests — the short version:

  • Backtick aggressively: file paths, class/method names, config keys (robot-council.members), commands (vendor/bin/pest), env vars, version arrows (`13.31.0` → `13.32.0`).
  • File paths are linked. The target is the absolute branch URL ([`<path>`](https://github.com/<owner>/<repo>/blob/<branch>/<path>), /tree/ for directories), exactly as the PR skill specifies; vendor refs may include line anchors (…/PackageServiceProvider.php#L20-L35).
  • Fenced code blocks (php, yaml, bash) quote the offending or proposed code in bug issues, and config excerpts elsewhere. Show before/after as two labelled blocks or inline-commented code, not a diff block.
  • Bullets are - only, nested with a 2-space indent. Bold-tag lead-ins (- **Per-key default** — …) categorize list items. Em-dash — separates a tag from its prose. Don't force-wrap prose.
  • Tables for naturally tabular data — config-key inventories, version matrices.

Labels

Apply the labels that match the issue's nature; multiple are normal. The label set is curated by hand, so re-list it before labeling rather than trusting the summary below:

gh api 'repos/{owner}/{repo}/labels?per_page=100' --jq '.[] | "\(.name)\t\(.description)"'

Apply only labels that exist, choosing by each label's description. If an issue needs a label that does not exist, name it in your proposal and ask before creating it; never create labels as a side effect of filing. There is no bug label: a bug is development with a symptom-first title and, where available, the Bug issue type.

  • development — code work in the package; the default, carried by most issues.
  • build — Composer, PHPStan, Pint, Pest configuration, GitHub Actions.
  • dependencies — applied by Dependabot to its update pull requests; not for hand-filed issues.
  • documentation — README and other prose.
  • security — security-sensitive work. An exploitable vulnerability does not go in a public issue at all: report it privately per security-audit.
  • ui — the package's published views or other user-visible output.
  • research — a spike whose deliverable is a documented decision, not code.
  • QA — verify expected behavior, usually paired with the build ticket it validates.
  • upstream — a report or request to file against a dependency, per filing-defects-across-repos.
  • test-flake — an intermittent failure that passes in isolation; not a real defect.
  • browser-tests — browser-driven test work (the Pest browser plugin is not installed yet).
  • quiet-machine — timing or measurement work that needs no concurrent runs, per measurement-parity.
  • macos-environment · windows-environment — must be completed on that platform.
  • handed-back — pull requests only: returned to the author by pre-merge-check with a finding outstanding.
  • afk · hitl · epic — execution mode, below. A hitl issue also carries its flavor: decision-fork (a design decision) or action-flavor (a real-world action).

Priority — fleet-facing / housekeeping (exactly one)

Every open issue also carries exactly one of these, and it decides the order work is taken in: fleet-facing comes first, housekeeping last.

  • fleet-facing — changes what an agent or operator on the fleet experiences: the service's API and behavior, the bridge and its follower, enrollment and credential storage, the tool list a harness sees, the messages a session reads, and the documented steps a fleet user follows. A measurement that decides whether one of those works on a platform counts too.
  • housekeeping — repository upkeep with no direct effect on the fleet: CI wiring, the release-notes generator, skills and rules, a test flake, or a measurement of code that has already been replaced.

Classify by who notices if it never ships. If only people working in the repository would, it is housekeeping, even when it changes shipped code or carries security. A cleanup of the service's API is fleet-facing, because the deployed bridge talks to it. An epic carries the label its slices mostly share. Still file housekeeping residue per closing-a-ticket; the label lowers its priority, not its right to a ticket.

Issue types

The robot-council organization enables the Task, Bug, and Feature issue types. Set exactly one on every issue: Bug for a bug, Feature for a feature, and Task for a research spike, a decision fork, a follow-up cleanup, or an epic. The templates declare the matching type: for issues filed through the web UI. When filing through REST, pass the type's name:

gh api -X POST 'repos/{owner}/{repo}/issues' -f title='…' -F [email protected] -f type=Feature

Read the type back after setting it. GitHub's REST description says a type set without push access is silently dropped, so the call succeeds either way: gh api 'repos/{owner}/{repo}/issues/<n>' --jq '.type.name'.

Execution mode — afk / hitl (exactly one — except on an epic)

Every open issue carries exactly one of afk / hitl. It tells whoever triages the tracker what kind of attention the issue needs to be completed: an autonomous coding agent, or a human.

The exception is epic. An issue labeled epic is exempt: it is a container whose slices each carry their own execution mode, not a unit of work anyone picks up and builds. An epic with neither label is correctly labeled — don't "fix" it. Apply hitl to an epic only where the container itself waits on a human.

  • afk (away-from-keyboard) — buildable autonomously: the design is pinned or already decided, or the fix is fully specified, so there is no genuine design fork to resolve in the issue's own scope. Most build slices, dependency bumps, refactors, and already-specified bug fixes are afk. The test: could a competent agent finish this alone, without a human deciding anything or doing anything outside the repo? If yes, afk.
  • hitl (human-in-the-loop) — completing it requires a human, for either reason: a human decision — an unresolved design fork (the issue carries a ## Questions to resolve / ## Decisions to resolve section), a policy / cost / go-no-go call, or a settled decision being re-opened; or a real-world action — a step outside the repo a coding agent can't perform (provisioning or credentialing, creating an external account, installing an app, publishing somewhere that needs a human's login). If a human must decide or act before the issue can close, hitl. There are only these two modes; fold real-world actions into hitl.

For the decision flavor, surface the fork the right way — lead with a compare/contrast packet, then the interactive prompt — following the design-decision-forks rule (don't restate it here).

A decision-fork ticket delivers two things and nothing else: the decision, recorded, and the follow-up tickets it produces. Build work the decision unblocks goes into those follow-ups, blocked by the fork through native dependency edges. Its acceptance criteria describe a decision being recorded and its follow-ups existing, never a build being done — a criterion naming a file to change belongs on a follow-up.

How to classify — a leaf issue takes the mode of what its own completion needs; don't inherit a blocker's mode (blockers carry their own labels). An epic is exempt — classify its slices, not the container. When genuinely on the fence, prefer hitl: it's safer to route an ambiguous issue to a human than to let an agent build past an unmade decision.

Keep the label current — flip it when the classification changes. Flip hitl → afk once the fork is decided and recorded on the issue — relabel (dropping decision-fork too) and reconcile the body in the same change (retire the ## Questions to resolve / ## Decisions to resolve section, tick the decision-recorded acceptance criterion). What remains on a fork ticket after the flip is filing its follow-ups, not building them. Flip afk → hitl the moment work surfaces a genuine design fork an agent shouldn't decide alone — relabel (adding decision-fork), add a ## Decisions to resolve section, and settle it with the user (per design-decision-forks) before building.

Epics and sub-issues

An epic is a container. It holds the shared context a body of work rests on and carries no deliverables of its own — no acceptance criteria, no "decisions taken here", nothing anyone could pick up and build. Every deliverable is a sub-issue. If a reader cannot tell what to do from a slice alone, the slice is under-written, not the epic.

That constraint is what makes an epic useful. A container with its own checklist competes with its slices, and the work ends up recorded in two places that drift.

When a ticket becomes an epic. Convert one when it has accumulated several independently shippable deliverables — usually visible as an acceptance-criteria list whose items would each make a reasonable ticket. Two shapes recur:

  • A decision plus its build. Spin the decision into its own gating slice rather than leaving it at the head of the container, so the fork is something a human can be handed and the epic is not itself hitl.
  • Measurement, decision, then build. File the measuring slices even when they are already done — see below.

File retroactive sub-issues for work already finished. When converting a ticket that has already produced results, give each completed piece its own sub-issue, record what it found in the body, and close it as completed. It costs a minute and it gives the finding a home: an epic body is a poor place for a measurement, because the next edit overwrites it.

Labels on a container. An epic takes the topic labels that describe its subject and not afk / hitl, nor labels that describe how its slices will be worked — those belong on the slices that earn them.

Wiring, and it is not the same thing as a dependency. A sub-issue says this is part of that; a blocked_by edge says this cannot start until that finishes. Most epics need both, and they are set independently:

id=$(gh api 'repos/{owner}/{repo}/issues/<child>' --jq '.id')     # numeric .id, NOT the issue number
gh api -X POST 'repos/{owner}/{repo}/issues/<parent>/sub_issues' -F sub_issue_id=$id

Sub-issues work across repositories. The endpoint keys on the global database id, so a slice living in another repository attaches to an epic here exactly like a local one; listing it only in prose loses it. Read the set back with GET …/sub_issues, and see github-api-budget for why these are REST calls.

Check for an existing issue first

Before creating any issue, search the tracker for one that already covers the same concept — match on the idea, not just the title, since an existing issue may be worded completely differently. Run a few searches with varied terms and include closed issues (a fixed or won't-fix issue still means don't refile):

# One block, because the control and the questions have to be the same instrument. The repository
# is resolved from the checkout once; the terms go in `-f` FIELDS rather than into the URL, so `gh`
# encodes them. `-X GET` because `gh api` sends POST as soon as a field is present, and `timeout`
# because the failure mode here is a hang rather than an error -- both explained below.
here=$(gh api 'repos/{owner}/{repo}' --jq .full_name)

for q in "robot-council" "<symptom or feature>" "<affected file / class / config key>" "<class of problem or label>"; do
  printf '%s -> ' "$q"
  timeout 25 gh api -X GET search/issues -f q="repo:$here $q" -f per_page=100 \
    --jq '"matches: \(.total_count) (showing \(.items|length))  from: \(.items[0].repository_url // "-" | sub(".*/repos/"; ""))",
          (.items[] | "  \(if .pull_request then "PR " else "iss" end) #\(.number)  \(.title)")' \
    || echo "NO ANSWER (rc=$?) -- not a clean check"
done

echo "checkout: $here   <- every from: above must equal this"

A search term written into the URL with a space in it HANGS, which is why the terms are passed as fields. Measured on 2026-09-24 with gh 2.92.0: gh api "search/issues?q=repo:…+two words" never returns. --verbose shows why -- it sends GET /search/issues?q=…+two words&per_page=1 HTTP/1.1, a request line with a raw space in it, and then waits for a reply that never comes. One such call ran 22 minutes before it was killed by hand. It is not the search endpoint's fault and not q's: the same raw space anywhere in a query string does it, measured with repos/{owner}/{repo}/issues?labels=a b. A space in the path is harmless by contrast -- repos/{owner}/{repo}/iss ues answers 404 promptly. The hazard is that the placeholders above are phrases, so following the recipe literally is what triggers it.

-f costs the placeholders, and that is the reason $here exists. gh substitutes {owner} and {repo} in a path, and in a field whose value is exactly the placeholder -- which is what writing-pull-requests's GraphQL recipe relies on with -F owner='{owner}'. It does not substitute them inside a longer field value: measured, -f q='repo:{owner}/{repo} term' sends the braces literally and GitHub answers Validation Failed. So the repository is read once through the path form and interpolated as text.

Read the matches: line before the rows. search/issues returns one page, so a query broader than the page size is answered with a silent prefix, and the truncated output is identical in shape to a query that genuinely found everything. In UAMS-Web/uams-statamic (measured 2026-09-06) a broad query reported hundreds of matches while a narrow one reported single digits, and nothing but the total distinguished the truncated page from the complete one.

A total greater than the rows shown means the check has not answered the question. Either paginate (add -f page=2, until a short page) or narrow the query until the total fits — "nothing matches" is not a conclusion you can draw from a truncated page. incomplete_results is not that signal: it reports a server-side timeout, not pagination, and it is false on truncated responses.

gh issue list and gh search issues exclude pull requests; the REST search endpoint above does not. That matters because a fix often ships as a pull request with no issue behind it, so the wrapper hides exactly the in-flight work the check exists to find — in UAMS-Web/uams-statamic (2026-09-05) a wrapper search returned the issue and never the pull request implementing it. The .pull_request key is what distinguishes the two kinds, hence the PR/iss prefix. The endpoint cannot phrase-match and it indexes comments; see github-api-budget.

Everything above guards against a query that is too NARROW. None of it guards against a query that did not RUN. Those produce the same output, and the second one is the easier mistake: a malformed --jq expression prints its parse error to stderr, gh exits non-zero, and a pipeline ending in || echo "(none)" prints exactly what a genuinely clean check prints. Observed while filing against robot-council/cli on 2026-09-21, where the check read:

=== duplicate check ===
  (none)

Two issues were filed on the strength of that line. Re-run afterwards with a working parser the check was genuinely clean — so no duplicate was filed, but that was luck rather than method, and the same output would have appeared had there been ten.

So show the query finding something, in the same invocation, before reading it finding nothing. A term certain to appear in the repository is enough, and it costs one call.

A count is not enough on its own, and the control has to name the repository that answered. A term common enough to be a reliable control is common enough to match in either repository, so a healthy total confirms the instrument while saying nothing about which tracker was asked — and a duplicate check aimed at the wrong one reads exactly like a clean one. Measured from a robot-council/core checkout on 2026-09-24: the term robot-council returned 197 matches aimed at this repository and 126 aimed at the other. Both look fine. repository_url off the first item is what tells them apart.

That is why robot-council is the first term in the block above rather than a separate invocation: the control and the questions then share one endpoint, one --jq, one quoting and one bound, so nothing can differ between them except the term.

Read the control's total and its from: first. If the total is 0, if the line is missing entirely, or if from: is not this checkout, stop — nothing below it is evidence. This is an-empty-result-is-not-evidence applied to the duplicate check; that rule owns the general form and the reasoning, and is not restated here.

from: reads - when a query matches nothing, which is the honest answer and not a failure: there is no item to take a repository from. That is why the total is read first — the two lines answer different questions.

Vary the terms across the symptom, the affected file/class/config key, and the class of problem — one query rarely surfaces a differently-worded duplicate. The repo: qualifier above is $here, read from the checkout, so these search the repository you are in; for work that spans repositories, substitute the other one for $here in a further query and expect its from: to name that repository rather than this one. If anything plausibly covers the concept: link it and skip, or add a comment / sharpen the existing issue — don't open a duplicate. Only file once you've confirmed nothing matches.

Archetypes at a glance

Pick the skeleton that matches the work:

  • Bug fix → lede (with discovery provenance) · ## The bug or ## Root cause (offending code) · ## Reproduction · ## Proposed fix (fix code) · ## Acceptance criteria (ends in a regression test) · ## References.
  • Feature → lede · ## Why · ## What we're building (+ behavior detail / tables) · ## Acceptance criteria · ## Out of scope · ## References.
  • Research spike → Research:-prefixed title · lede ("Research spike. … deliverable is a documented decision, not code") · ## Why this is a spike · ## Questions to resolve · ## Acceptance criteria (decisions + go/no-go) · ## References.
  • Follow-up cleanup → Follow-up to #N. lede · ## Scope · short ## Acceptance criteria. Follow-up to #N. is provenance and stays; any dependency on #N is an edge.

Tone and voice

  • Technical, prose-driven, specific. Lead with why, then what. Concrete file paths, config keys, commands, URLs, error strings over generalities.
  • Bound scope deliberately — ## Out of scope is part of writing a good issue, not an afterthought.
  • No exclamation marks. Use *emphasis* sparingly, only on the word that carries the sentence.
  • Impersonal voice throughout — every artifact posts under one account, so no I/my/you/your, and no vouching stance. See impersonal-voice-in-github-artifacts.
  • No emoji — not in the title, the body, or any comment. Use words: a bolded clause says what a glyph was standing in for, and a glyph breaks downstream where nothing can report it. See no-emoji-in-durable-records.

Example shape

A compact bug issue demonstrating the core conventions. Illustrative only: members() and the config key are invented to show the shape.

Title: RobotCouncil::members() returns null (not []) when no members are configured

````
Discovered while writing feature tests for the `robot-council` command. `RobotCouncil\RobotCouncil::members()` declares an `array` return type, but when `robot-council.members` is absent from config it returns `null`, so PHP throws a `TypeError` and the command fails before printing anything.

## Root cause

[`src/RobotCouncil.php`](https://github.com/robot-council/core/blob/main/src/RobotCouncil.php) -- **the package's file, named because the example is its**; the application has no `src/`:

```php
public function members(): array
{
    return config('robot-council.members');   // null when the key is absent
}
```

## Proposed fix

Default the lookup to an empty array so the signature holds:

```php
return config('robot-council.members', []);
```

## Acceptance criteria

- [ ] `RobotCouncil::members()` returns `[]` when `robot-council.members` is absent.
- [ ] `RobotCouncil::members()` still returns the configured list unchanged when the key is present.
- [ ] A Pest test covers the absent-key and configured-list cases.
````

(Lede opens with discovery provenance and leads with the user-visible failure. ## Root cause quotes the offending code; ## Proposed fix shows the one-line change. Acceptance criteria are observable outcomes that bound both the fixed and the unaffected behavior and close with a regression test. No AI disclaimer. File path links to the absolute branch URL.)

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console