| name | file-github-bug |
| description | Use when filing a detailed bug report against a GitHub repo with evidence, optionally adding it to a GitHub Project (v2), setting milestone from current branch, and marking initial status. Reads repo/project metadata from .local-projects.json so the skill is repo-agnostic. |
| allowed-tools | Bash, Read, Grep, Glob |
| argument-hint | <target-project-name> |
File GitHub Bug
Create a detailed, unambiguous bug report as a GitHub issue, optionally adding it to a GitHub Project (v2) and setting status. Repo and project metadata are looked up by name from .local-projects.json; the skill itself is repo-agnostic.
Inputs
- target (argument): The project name from
.local-projects.json whose repo will receive the issue. If omitted, ask the user.
- Conversation context providing: bug description, evidence (logs, payloads, code refs), reproduction steps, expected vs. actual behavior, hypotheses.
Configuration
.local-projects.json (walked up from pwd) supplies:
{
"projects": [{
"name": "<target>",
"github": {
"owner": "<gh-login>",
"repo": "<repo-name>",
"issues_project": {
"number": 2,
"owner": "<gh-login>",
"id": "PVT_...",
"fields": {
"status": {
"id": "PVTSSF_...",
"options": {
"backlog": "<option-id>",
"this_milestone": "<option-id>",
"in_progress": "<option-id>",
"done": "<option-id>"
},
"default_option": "this_milestone"
}
}
}
}
}]
}
If issues_project is absent, the skill creates the issue without adding it to a project.
Process
1. Look up target
CONFIG=$(find_up .local-projects.json)
TARGET="$1"
OWNER=$(jq -r --arg t "$TARGET" '.projects[] | select(.name==$t) | .github.owner' "$CONFIG")
REPO=$(jq -r --arg t "$TARGET" '.projects[] | select(.name==$t) | .github.repo' "$CONFIG")
(find_up walks parent directories until it finds the named file. Implement inline if needed.)
If OWNER/REPO are empty, error and ask the user to add the project to .local-projects.json.
2. Gather evidence
Collect from the current conversation:
- API request/response payloads
- Log excerpts with timestamps
- Expected vs. actual behavior
- Reproduction steps
- Affected endpoints, fields, or operations
- Root cause hypotheses with code references
3. Determine milestone
Compare the current git branch to milestones on the target repo, in order:
- The full branch name verbatim.
- A trailing semver (
X.Y.Z, optionally with a -prerelease suffix
like -rc.1) extracted from the end of the branch name. Handles the
common convention where branches are named dev/1.4.0 or
release/1.4.0-rc.1 but milestones use bare semver.
- If still no match, repeat steps 1–2 against the parent branch
(the nearest local branch by
merge-base distance from HEAD,
excluding main). Handles main → dev/1.4.0 → feature/foo cases
where the feature branch itself carries no version info.
- If still no match, do not set a milestone.
main is the "no milestone" sentinel — never resolve a milestone for
it, and never walk to a parent above it.
The parent lookup walks only one level. If the parent is itself a
feature branch (e.g. a feature based on another feature), no further
walk happens.
BRANCH=$(git branch --show-current)
MILESTONE=""
extract_semver() {
echo "$1" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' || true
}
find_parent_branch() {
local self="$1"
git for-each-ref --format='%(refname:short)' refs/heads/ \
| while read -r b; do
[ "$b" = "$self" ] && continue
[ "$b" = "main" ] && continue
local mb dist
mb=$(git merge-base "$b" HEAD 2>/dev/null) || continue
dist=$(git rev-list --count "$mb..HEAD" 2>/dev/null) || continue
echo "$dist $b"
done \
| sort -n \
| head -1 \
| awk '{print $2}'
}
if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ]; then
MILESTONES=$(gh api "repos/$OWNER/$REPO/milestones?state=open" --jq '.[].title')
CANDIDATES=("$BRANCH" "$(extract_semver "$BRANCH")")
matched=""
for candidate in "${CANDIDATES[@]}"; do
[ -z "$candidate" ] && continue
if echo "$MILESTONES" | grep -Fxq "$candidate"; then
MILESTONE="$candidate"
matched=1
break
fi
done
if [ -z "$matched" ]; then
PARENT=$(find_parent_branch "$BRANCH")
if [ -n "$PARENT" ] && [ "$PARENT" != "main" ]; then
for candidate in "$PARENT" "$(extract_semver "$PARENT")"; do
[ -z "$candidate" ] && continue
if echo "$MILESTONES" | grep -Fxq "$candidate"; then
MILESTONE="$candidate"
break
fi
done
fi
fi
fi
Matching is exact (case preserved, no partial matches) so unrelated
branches don't pick up a milestone by accident. The parent-branch
heuristic uses merge-base against each local branch and picks the
one whose merge-base is closest to HEAD; this works without any branch
metadata or upstream configuration, and tolerates the parent having
advanced past the fork point.
Parent-walk caveats:
- If you've based a feature branch off another feature branch
(e.g.
feature/b off feature/a off dev/1.4.0), the parent
resolves to feature/a, not dev/1.4.0. No further walk happens.
- If two local branches have the same tip distance, sort order picks
one — typically not an issue in practice.
- On a fresh checkout with only
main present, no parent is found
and no milestone is set.
4. Create the issue
Title: fix: <concise description> (Conventional Commit prefix).
Labels: always bug. Add api if the bug involves an API endpoint.
Body:
## Summary
<1-3 sentence description>
## Steps to Reproduce
1. <step>
2. <step>
## Expected Behavior
<what should happen>
## Actual Behavior
<what actually happens>
## Evidence
<logs, payloads, screenshots, code refs>
### Request
```json
<payload if applicable>
Response
<payload if applicable>
Possible Cause
Impact
<severity, user-facing impact>
Environment
<endpoint, client version, content-type, etc.>
Omit sections that don't apply.
```bash
gh issue create --repo "$OWNER/$REPO" \
--title "fix: <title>" \
--label "bug,api" \
${MILESTONE:+--milestone "$MILESTONE"} \
--body "$(cat <<'EOF'
<body content>
EOF
)"
Capture the returned issue URL and extract the issue number.
5. Add to GitHub Project (if configured)
PROJ_NUM=$(jq -r --arg t "$TARGET" '.projects[] | select(.name==$t) | .github.issues_project.number' "$CONFIG")
PROJ_OWNER=$(jq -r --arg t "$TARGET" '.projects[] | select(.name==$t) | .github.issues_project.owner' "$CONFIG")
PROJ_ID=$(jq -r --arg t "$TARGET" '.projects[] | select(.name==$t) | .github.issues_project.id' "$CONFIG")
if [ "$PROJ_NUM" != "null" ] && [ -n "$PROJ_NUM" ]; then
gh project item-add "$PROJ_NUM" --owner "$PROJ_OWNER" --url "$ISSUE_URL"
fi
6. Set initial status (if configured)
The default status is whatever default_option in the config is set to (e.g. this_milestone). Callers may override by passing a different option key.
STATUS_FIELD_ID=$(jq -r --arg t "$TARGET" '.projects[] | select(.name==$t) | .github.issues_project.fields.status.id' "$CONFIG")
DEFAULT_KEY=$(jq -r --arg t "$TARGET" '.projects[] | select(.name==$t) | .github.issues_project.fields.status.default_option' "$CONFIG")
OPTION_ID=$(jq -r --arg t "$TARGET" --arg k "$DEFAULT_KEY" \
'.projects[] | select(.name==$t) | .github.issues_project.fields.status.options[$k]' "$CONFIG")
ITEM_ID=$(gh project item-list "$PROJ_NUM" --owner "$PROJ_OWNER" --format json --limit 200 \
| jq -r --argjson n "$ISSUE_NUMBER" '.items[] | select(.content.number==$n) | .id')
gh project item-edit \
--project-id "$PROJ_ID" \
--id "$ITEM_ID" \
--field-id "$STATUS_FIELD_ID" \
--single-select-option-id "$OPTION_ID"
7. Report
Created: <issue_url>
Labels: bug[, api]
Milestone: <milestone or "none">
Project: <project-name> (<status-key>)
Output
The created issue URL.
Error Handling
| Error | Behavior |
|---|
gh not authenticated | Tell user to run gh auth login. |
Target not in .local-projects.json | Error with the list of known names. |
issues_project absent | Create issue, skip project add/status, note in output. |
| Milestone not found | Create without milestone. |
gh project item-add fails | Report; the issue still exists. |
| Status update fails | Report; status stays at the project's default. |
Example
Target: tmi (server repo)
Branch: release/1.3.0
Evidence: PATCH /threat_models/.../diagrams/... sends shape:"actor",
server responds with shape:"store"; logs show mismatch.
Output:
Created: https://github.com/ericfitz/tmi/issues/166
Labels: bug, api
Milestone: release/1.3.0
Project: tmi (this_milestone)
Implementation Notes
- Evidence quality matters: include actual payloads and field values, not paraphrases.
- Conventional Commit prefix: always
fix: for bug reports.
- Branch → milestone: tried in order — full branch name, trailing semver (
X.Y.Z with optional -prerelease) extracted from the branch name, then the same two candidates against the parent branch (one level up, via merge-base nearest-ancestor). All matches are exact against open milestone titles; no fuzzy matching beyond the explicit candidates above, to avoid picking up a milestone by accident.
- Project/field IDs: stable for a project's lifetime. If the project is rebuilt, refresh values in
.local-projects.json with gh project field-list <number> --owner <owner>.
- Field ID discovery: to populate the config initially, run
gh project field-list <number> --owner <owner>.