Installation
Install with CLI
Recommended
gh skills-hub install bug-receipt Don't have the extension? Run gh extension install samueltauil/skills-hub first.
Download and extract to your repository:
.github/skills/bug-receipt/ Extract the ZIP to .github/skills/ in your repo. The folder name must match bug-receipt for Copilot to auto-discover it.
Skill Files (5)
SKILL.md 4.3 KB
---
name: bug-receipt
description: 'Close defects and incidents with a BUG RECEIPT and VERIFIED, PARTIAL, or BLOCKED status after diagnosis, repair, or recovery.'
metadata:
version: "1.4.1"
---
# Bug Receipt
## Mandatory closeout output
For every bug or incident closeout decision, return the complete receipt below as the entire user-facing result, even when the user requests a concise reply or does not name this format. Concision shortens field values; it never removes or renames a row. Do not replace the receipt with prose.
```text
BUG RECEIPT ยท VERIFIED | PARTIAL | BLOCKED
Problem <observed defect and intended behavior>
Baseline <failing interaction or command and decisive result; or not run>
Root cause <proven mechanism; or unproven hypothesis>
Change <responsible change; or none>
Proof <supplied or executed check: result; include every decisive layer>
Gaps <none; or exact missing proof and single next experiment/package>
Source executed now | supplied | mixed
```
Use `not run`, `unproven`, or `none` explicitly. Never omit a row to make the receipt look complete.
## Establish the evidence boundary
Before editing, record the observed problem, intended behavior, strongest direct check, and evidence source: `executed now`, `supplied`, or `mixed`. Never imply that supplied evidence was executed in the current run.
Keep evidence privacy-minimal. Redact credentials, tokens, cookies, personal data, private URLs, and sensitive payloads; preserve only the identifiers and excerpts needed to reproduce or correlate the failure.
Reproduce the failure with the narrowest safe check when possible. If reproduction is unavailable, preserve the evidence obtained and cap the result at `PARTIAL` or `BLOCKED`.
## Trace and repair
1. Follow the live owner path from input to symptom.
2. Separate observed facts, bounded inferences, and gaps.
3. Require a concrete location or runtime transition before naming root cause.
4. Make the smallest responsible change; avoid unrelated cleanup, retries, silent fallbacks, and fixture-specific exceptions.
Do not convert a plausible patch, stale log, source read, or passing build into proof of the user-visible behavior.
## Close the proof loop
Run only checks required by the affected contract:
- original reproduction or direct acceptance check;
- nearest negative or regression check;
- affected build or integration gate;
- real UI, API, persistence, concurrency, or runtime path when the claim crosses that boundary.
Use these decisive boundaries:
| Surface | Required direct proof |
| --- | --- |
| Logic or failing test | Original failing input or focused test now passes |
| UI behavior | Real interaction plus relevant console and network observation |
| API or integration | Request, response, and responsible service behavior |
| Persistence | Write/read or reload round trip through the real owner path |
| Race or lifecycle | Repeated concurrent trigger; zero-or-one success; affected-row and transaction evidence; final invariant |
| Cross-system blocker | One sanitized failing request/response with timestamp or request ID, edge and application logs, and identity-provider logs when the trace reaches that owner |
## Assign status
- `VERIFIED`: observed baseline, concrete cause, responsible change, all declared checks passed, no material gap.
- `PARTIAL`: useful evidence exists, but a required proof layer is missing or inconclusive.
- `BLOCKED`: a specific external condition prevents reproduction, repair, or proof.
For `PARTIAL` or `BLOCKED`, name the single minimal experiment or correlated evidence package that closes the decisive gap. Never invent a command, observation, count, location, or result.
For a machine-readable receipt or CI integration, read [references/receipt-contract.md](references/receipt-contract.md) and conform to its JSON fields, evidence-source marker, compatibility rule, and status invariants.
When a JSON artifact is requested, start from [assets/receipt.template.json](assets/receipt.template.json), write it to a task-owned path, and validate it with `node scripts/validate-receipt.mjs <receipt.json>` from this skill directory. Do not commit the generated receipt unless the user requests it.
## Source and license
Originally published at https://github.com/lMysticl/bug-receipt under the MIT License.
assets/
receipt.template.json 0.6 KB
{
"version": 2,
"status": "partial",
"evidenceSource": "supplied",
"problem": "Describe the observed defect and intended behavior.",
"baseline": {
"command": "Record the exact reproduction command or interaction.",
"result": "not-run",
"evidence": "State the decisive observation, or why it could not be obtained."
},
"rootCause": {
"summary": "State the evidence-backed mechanism, or mark it unresolved.",
"evidence": []
},
"changes": [],
"verification": [],
"gaps": [
"Replace this with the exact missing proof layer."
]
}
references/
receipt-contract.md 1.8 KB
# Machine-readable receipt contract
Use JSON only when the user, CI, or another tool needs a structured artifact. Keep the normal final answer human-readable.
## Required fields
- `version`: integer `2` for new receipts. Version `1` remains accepted for compatibility.
- `status`: `verified`, `partial`, or `blocked`.
- `evidenceSource`: `executed-now`, `supplied`, or `mixed` (required in version `2`).
- `problem`: concise defect and intended behavior.
- `baseline`: object with `command`, `result`, and `evidence`.
- `rootCause`: object with `summary` and at least one evidence item for `verified`.
- `changes`: array of `{ "file", "summary" }` objects.
- `verification`: array of `{ "command", "result", "evidence" }` objects.
- `gaps`: array of explicit missing proof statements.
Baseline results are `failed`, `observed`, or `not-run`. Verification results are `passed`, `failed`, or `not-run`.
## Status invariants
For `verified`:
- Require an observed baseline: `failed` or `observed`, never `not-run`.
- Require at least one concrete root-cause evidence item with `location` and `observation`.
- Require at least one changed file or artifact.
- Require at least one verification item.
- Require every verification result to be `passed`.
- Require `gaps` to be empty.
For `partial`:
- Preserve all evidence obtained.
- Put every missing or inconclusive proof layer in `gaps`.
- Never convert an unrun check into `passed`.
For `blocked`:
- Require at least one gap naming the external blocking condition.
- Leave unperformed work empty or mark it `not-run`; do not speculate about the result.
Validate against [receipt.schema.json](receipt.schema.json), run `node scripts/validate-receipt.mjs <file>` from the skill directory, pipe JSON to `node scripts/validate-receipt.mjs - --json`, or use `bug-receipt check <file>` when the package CLI is installed.
receipt.schema.json 3.5 KB
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://lmysticl.github.io/bug-receipt/receipt.schema.json",
"title": "Bug Receipt",
"description": "A machine-readable evidence receipt for a software bug fix.",
"type": "object",
"additionalProperties": false,
"required": ["version", "status", "problem", "baseline", "rootCause", "changes", "verification", "gaps"],
"properties": {
"version": { "enum": [1, 2] },
"status": { "enum": ["verified", "partial", "blocked"] },
"evidenceSource": { "enum": ["executed-now", "supplied", "mixed"] },
"problem": { "$ref": "#/$defs/nonEmptyString" },
"baseline": {
"type": "object",
"additionalProperties": false,
"required": ["command", "result", "evidence"],
"properties": {
"command": { "$ref": "#/$defs/nonEmptyString" },
"result": { "enum": ["failed", "observed", "not-run"] },
"evidence": { "$ref": "#/$defs/nonEmptyString" }
}
},
"rootCause": {
"type": "object",
"additionalProperties": false,
"required": ["summary", "evidence"],
"properties": {
"summary": { "$ref": "#/$defs/nonEmptyString" },
"evidence": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["location", "observation"],
"properties": {
"location": { "$ref": "#/$defs/nonEmptyString" },
"observation": { "$ref": "#/$defs/nonEmptyString" }
}
}
}
}
},
"changes": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["file", "summary"],
"properties": {
"file": { "$ref": "#/$defs/nonEmptyString" },
"summary": { "$ref": "#/$defs/nonEmptyString" }
}
}
},
"verification": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["command", "result", "evidence"],
"properties": {
"command": { "$ref": "#/$defs/nonEmptyString" },
"result": { "enum": ["passed", "failed", "not-run"] },
"evidence": { "$ref": "#/$defs/nonEmptyString" }
}
}
},
"gaps": { "type": "array", "items": { "$ref": "#/$defs/nonEmptyString" } }
},
"$defs": {
"nonEmptyString": { "type": "string", "minLength": 1, "pattern": "\\S" }
},
"allOf": [
{
"if": { "properties": { "version": { "const": 2 } }, "required": ["version"] },
"then": {
"properties": { "evidenceSource": { "enum": ["executed-now", "supplied", "mixed"] } },
"required": ["evidenceSource"]
}
},
{
"if": { "properties": { "status": { "const": "verified" } }, "required": ["status"] },
"then": {
"properties": {
"baseline": { "type": "object", "properties": { "result": { "enum": ["failed", "observed"] } } },
"rootCause": { "type": "object", "properties": { "evidence": { "type": "array", "minItems": 1 } } },
"changes": { "type": "array", "minItems": 1 },
"verification": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "result": { "const": "passed" } } } },
"gaps": { "type": "array", "maxItems": 0 }
}
}
},
{
"if": { "properties": { "status": { "enum": ["partial", "blocked"] } }, "required": ["status"] },
"then": { "properties": { "gaps": { "type": "array", "minItems": 1 } } }
}
]
}
scripts/
validate-receipt.mjs 7.7 KB
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const statuses = new Set(['verified', 'partial', 'blocked'])
const evidenceSources = new Set(['executed-now', 'supplied', 'mixed'])
const baselineResults = new Set(['failed', 'observed', 'not-run'])
const verificationResults = new Set(['passed', 'failed', 'not-run'])
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0
export const sampleReceipt = {
version: 2,
status: 'verified',
evidenceSource: 'executed-now',
problem: 'A 10% checkout discount returns 100 instead of 90 after currency rounding.',
baseline: {
command: 'npm test -- discount.test.ts',
result: 'failed',
evidence: 'Expected 90, received 100.',
},
rootCause: {
summary: 'The subtotal was rounded before the percentage discount was applied.',
evidence: [{ location: 'src/pricing.ts:42', observation: 'roundCurrency(subtotal) was passed into applyDiscount().' }],
},
changes: [{ file: 'src/pricing.ts', summary: 'Apply the discount to the subtotal before currency rounding.' }],
verification: [
{ command: 'npm test -- discount.test.ts', result: 'passed', evidence: '1 test passed.' },
{ command: 'npm test', result: 'passed', evidence: '42 tests passed.' },
],
gaps: [],
}
export function validateReceipt(receipt) {
const issues = []
const add = (path, message) => issues.push({ path, message })
const rejectUnknown = (value, allowed, path) => {
if (!isObject(value)) return
for (const key of Object.keys(value)) {
if (!allowed.has(key)) add(path ? `${path}.${key}` : key, 'Unknown field.')
}
}
if (!isObject(receipt)) return { valid: false, issues: [{ path: '$', message: 'Receipt must be a JSON object.' }] }
rejectUnknown(receipt, new Set(['version', 'status', 'evidenceSource', 'problem', 'baseline', 'rootCause', 'changes', 'verification', 'gaps']), '')
if (receipt.version !== 1 && receipt.version !== 2) add('version', 'Must equal 1 or 2.')
if (!statuses.has(receipt.status)) add('status', 'Must be verified, partial, or blocked.')
if (receipt.evidenceSource !== undefined && !evidenceSources.has(receipt.evidenceSource)) add('evidenceSource', 'Must be executed-now, supplied, or mixed.')
if (receipt.version === 2 && !evidenceSources.has(receipt.evidenceSource)) add('evidenceSource', 'Version 2 requires an evidence source.')
if (!nonEmpty(receipt.problem)) add('problem', 'Must be a non-empty string.')
if (!isObject(receipt.baseline)) {
add('baseline', 'Must be an object.')
} else {
rejectUnknown(receipt.baseline, new Set(['command', 'result', 'evidence']), 'baseline')
if (!nonEmpty(receipt.baseline.command)) add('baseline.command', 'Must be a non-empty string.')
if (!baselineResults.has(receipt.baseline.result)) add('baseline.result', 'Must be failed, observed, or not-run.')
if (!nonEmpty(receipt.baseline.evidence)) add('baseline.evidence', 'Must be a non-empty string.')
}
if (!isObject(receipt.rootCause)) {
add('rootCause', 'Must be an object.')
} else {
rejectUnknown(receipt.rootCause, new Set(['summary', 'evidence']), 'rootCause')
if (!nonEmpty(receipt.rootCause.summary)) add('rootCause.summary', 'Must be a non-empty string.')
if (!Array.isArray(receipt.rootCause.evidence)) {
add('rootCause.evidence', 'Must be an array.')
} else {
receipt.rootCause.evidence.forEach((entry, index) => {
if (!isObject(entry)) return add(`rootCause.evidence[${index}]`, 'Must be an object.')
rejectUnknown(entry, new Set(['location', 'observation']), `rootCause.evidence[${index}]`)
if (!nonEmpty(entry.location)) add(`rootCause.evidence[${index}].location`, 'Must be a non-empty string.')
if (!nonEmpty(entry.observation)) add(`rootCause.evidence[${index}].observation`, 'Must be a non-empty string.')
})
}
}
if (!Array.isArray(receipt.changes)) {
add('changes', 'Must be an array.')
} else {
receipt.changes.forEach((entry, index) => {
if (!isObject(entry)) return add(`changes[${index}]`, 'Must be an object.')
rejectUnknown(entry, new Set(['file', 'summary']), `changes[${index}]`)
if (!nonEmpty(entry.file)) add(`changes[${index}].file`, 'Must be a non-empty string.')
if (!nonEmpty(entry.summary)) add(`changes[${index}].summary`, 'Must be a non-empty string.')
})
}
if (!Array.isArray(receipt.verification)) {
add('verification', 'Must be an array.')
} else {
receipt.verification.forEach((entry, index) => {
if (!isObject(entry)) return add(`verification[${index}]`, 'Must be an object.')
rejectUnknown(entry, new Set(['command', 'result', 'evidence']), `verification[${index}]`)
if (!nonEmpty(entry.command)) add(`verification[${index}].command`, 'Must be a non-empty string.')
if (!verificationResults.has(entry.result)) add(`verification[${index}].result`, 'Must be passed, failed, or not-run.')
if (!nonEmpty(entry.evidence)) add(`verification[${index}].evidence`, 'Must be a non-empty string.')
})
}
if (!Array.isArray(receipt.gaps) || receipt.gaps.some((gap) => !nonEmpty(gap))) add('gaps', 'Must be an array of non-empty strings.')
if (receipt.status === 'verified') {
if (receipt.baseline?.result === 'not-run') add('baseline.result', 'Verified requires an observed baseline.')
if (!Array.isArray(receipt.rootCause?.evidence) || receipt.rootCause.evidence.length === 0) add('rootCause.evidence', 'Verified requires concrete root-cause evidence.')
if (!Array.isArray(receipt.changes) || receipt.changes.length === 0) add('changes', 'Verified requires at least one changed file or artifact.')
if (!Array.isArray(receipt.verification) || receipt.verification.length === 0) add('verification', 'Verified requires at least one verification check.')
if (Array.isArray(receipt.verification) && receipt.verification.some((entry) => entry?.result !== 'passed')) add('verification', 'Every verification check must pass for verified status.')
if (Array.isArray(receipt.gaps) && receipt.gaps.length > 0) add('gaps', 'Verified status cannot contain proof gaps.')
}
if (receipt.status === 'partial' && Array.isArray(receipt.gaps) && receipt.gaps.length === 0) add('gaps', 'Partial status must name at least one missing proof layer.')
if (receipt.status === 'blocked' && Array.isArray(receipt.gaps) && receipt.gaps.length === 0) add('gaps', 'Blocked status must name the external blocking condition.')
return { valid: issues.length === 0, issues }
}
async function main() {
const path = process.argv[2]
if (!path) throw new Error('Usage: node scripts/validate-receipt.mjs <receipt.json> [--json]')
let input = ''
if (path === '-') {
process.stdin.setEncoding('utf8')
for await (const chunk of process.stdin) input += chunk
} else {
input = await readFile(resolve(path), 'utf8')
}
const receipt = JSON.parse(input)
const result = validateReceipt(receipt)
if (process.argv.includes('--json')) {
process.stdout.write(`${JSON.stringify(result)}\n`)
} else if (result.valid) {
process.stdout.write(`โ ${path} is a valid ${receipt.status.toUpperCase()} bug receipt.\n`)
} else {
process.stderr.write(`โ ${path} is not a valid bug receipt:\n`)
for (const issue of result.issues) process.stderr.write(` ${issue.path}: ${issue.message}\n`)
}
process.exitCode = result.valid ? 0 : 1
}
const invokedUrl = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''
if (import.meta.url === invokedUrl) {
main().catch((error) => {
process.stderr.write(`bug-receipt: ${error.message}\n`)
process.exitCode = 2
})
}
License (MIT)
View full license text
MIT License Copyright GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.