Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/pr-check-managed-files-hosted.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ on:
- .editorconfig
- .editorconfig-checker.json
- .gitattributes
- .github/ISSUE_TEMPLATE/agent-ready.yml
- .github/ISSUE_TEMPLATE/bug-report.yml
- .github/ISSUE_TEMPLATE/feature-request.yml
- .github/ISSUE_TEMPLATE/task.yml
- .github/actionlint.yaml
- .github/standards/dependabot-policy/policy.json
- .github/standards/github-actions-conventions/vocabulary.json
- .github/standards/github-actions-conventions/vocabulary.schema.json
- .github/standards/issue-body-contract/contract.json
- .github/standards/pyright/pyrightconfig.json
- .github/standards/runner-policy/package-lock.json
- .github/standards/runner-policy/package.json
Expand All @@ -69,6 +74,7 @@ on:
- .shellcheckrc
- REVIEW.md
- _typos.toml
- docs/contracts/issue-body/contract.json
- docs/conventions/source-control/commit-convention.yml
- docs/standards/README.md
- docs/standards/bulk-mutation.md
Expand Down
235 changes: 235 additions & 0 deletions docs/contracts/issue-body/contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
{
"schemaVersion": 1,
"headingLevels": [2, 3],
"emptyAnswer": "_No response_",
"floorLabels": ["needs-triage"],
"types": {
"bug": {
"form": "bug-report.yml",
"name": "Bug Report",
"description": "Report an unexpected problem or behavior.",
"nativeType": "Bug",
"labels": ["needs-triage"],
"fields": [
{
"id": "steps",
"heading": "Steps to reproduce",
"kind": "textarea",
"required": true,
"description": "Minimal steps that reproduce the behavior.",
"placeholder": "1. ...\n2. ...\n3. ..."
},
{
"id": "expected",
"heading": "Expected behavior",
"kind": "textarea",
"required": true,
"description": "What should happen?"
},
{
"id": "actual",
"heading": "Actual behavior",
"kind": "textarea",
"required": true,
"description": "What actually happens? Include error messages, stack traces, or screenshots."
},
{
"id": "severity",
"heading": "Severity",
"kind": "dropdown",
"required": true,
"description": "How bad is it? Say why under Severity justification.",
"options": ["low", "medium", "high", "critical"]
},
{
"id": "severity-justification",
"heading": "Severity justification",
"kind": "textarea",
"required": true,
"description": "One sentence: the blast radius, what it blocks, or the data at risk."
},
{
"id": "fix-location",
"heading": "Suggested fix location",
"kind": "input",
"required": false,
"description": "The file and function or class where the fix likely belongs, if known. No patch."
},
{
"id": "environment",
"heading": "Environment",
"kind": "input",
"required": false,
"description": "OS, runtime and tool versions, and anything else relevant."
}
]
},
"feature": {
"form": "feature-request.yml",
"name": "Feature Request",
"description": "Propose a new feature or enhancement.",
"nativeType": "Feature",
"labels": ["needs-triage"],
"fields": [
{
"id": "user-story",
"heading": "User story",
"kind": "textarea",
"required": true,
"description": "Who benefits and why?",
"placeholder": "As a [role], I want [capability] so that [benefit]."
},
{
"id": "acceptance-criteria",
"heading": "Acceptance criteria",
"kind": "textarea",
"required": true,
"description": "How do we know this is done? One verifiable check per line.",
"placeholder": "- [ ] Criterion 1\n- [ ] Criterion 2"
},
{
"id": "area",
"heading": "Affected area / component",
"kind": "input",
"required": false,
"description": "Which part of the project would this affect?"
}
]
},
"task": {
"form": "task.yml",
"name": "Task",
"description": "A chore, maintenance task, audit, or other housekeeping item.",
"nativeType": "Task",
"labels": ["needs-triage"],
"fields": [
{
"id": "context",
"heading": "Context",
"kind": "textarea",
"required": true,
"description": "What surfaced this, and what leaving it costs."
},
{
"id": "proposed-work",
"heading": "Proposed work",
"kind": "textarea",
"required": true,
"description": "The concrete next actions.",
"placeholder": "- ..."
},
{
"id": "acceptance-criteria",
"heading": "Acceptance criteria",
"kind": "textarea",
"required": true,
"description": "One verifiable check per line.",
"placeholder": "- [ ] ..."
},
{
"id": "references",
"heading": "References",
"kind": "textarea",
"required": false,
"description": "Rules, files, pull requests, or docs this relates to."
}
]
},
"agent-ready": {
"form": "agent-ready.yml",
"name": "Agent-Ready Work Item",
"description": "Work written as an agent brief. Triage decides its type and whether it is agent-ready.",
"nativeType": null,
"labels": ["needs-triage"],
"fields": [
{
"id": "summary",
"heading": "Summary",
"kind": "input",
"required": true,
"description": "The change, in one line."
},
{
"id": "root-cause",
"heading": "Root cause",
"kind": "input",
"required": false,
"description": "Bugs only: unknown, or known: the cause in one line."
},
{
"id": "current-behavior",
"heading": "Current behavior",
"kind": "textarea",
"required": true,
"description": "What the code does today."
},
{
"id": "desired-behavior",
"heading": "Desired behavior",
"kind": "textarea",
"required": true,
"description": "What is true once this lands, edge cases and failure handling included. The end state, not edit instructions."
},
{
"id": "key-interfaces",
"heading": "Key interfaces",
"kind": "textarea",
"required": false,
"description": "The types, signatures, config keys, or observable behaviors that change, named by contract rather than file path or line."
},
{
"id": "acceptance-criteria",
"heading": "Acceptance criteria",
"kind": "textarea",
"required": true,
"description": "Checks that each give a yes or no.",
"placeholder": "- [ ] ..."
},
{
"id": "out-of-scope",
"heading": "Out of scope",
"kind": "textarea",
"required": true,
"description": "The neighboring code and features this change leaves as they are."
}
]
},
"decomposed-item": {
"form": null,
"name": "Decomposed work item",
"description": "A vertical slice /work-items:decompose publishes, born triaged; no public form.",
"nativeType": null,
"labels": [],
"fields": [
{
"id": "outcome",
"heading": "Outcome",
"kind": "textarea",
"required": true,
"description": "The one observable change a user gets when this slice merges, end to end."
},
{
"id": "key-interfaces",
"heading": "Key interfaces",
"kind": "textarea",
"required": true,
"description": "The design this slice touches, named by contract, or None."
},
{
"id": "done-when",
"heading": "Done when",
"kind": "textarea",
"required": true,
"description": "The slice's acceptance criteria, each a check with a pass or fail answer."
},
{
"id": "out-of-scope",
"heading": "Out of scope",
"kind": "textarea",
"required": true,
"description": "Adjacent work this slice must not change."
}
]
}
}
}
6 changes: 3 additions & 3 deletions docs/standards/issue-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The naming grammar above governs labels only. Issue titles are free text: there

**Structured metadata, not title prefixes.** Set the type with `--type` or the issue form's Types field; set area and other axes with labels. PR titles are unchanged, remaining Conventional Commits enforced by the `pr-contract` composite inside the required `ci-status` check, because squash-merge makes the PR title the default-branch subject line. Issue titles and PR titles serve different layers and follow different rules.

**Issue forms set no title prefix.** GitHub's issue-form examples ship a `title: "[Bug]: "` default; forms here leave `title` empty and set the type through the form's `type:` key instead, so every issue reads the same.
**Issue forms set no title prefix.** GitHub's issue-form examples ship a `title: "[Bug]: "` default; forms here leave `title` empty and set the type through the form's `type:` key instead, so every issue reads the same. The one exception is the agent-ready work-item form, which sets no type: its work can be a bug, a feature or a task, so triage sets the type (the [issue-body contract](https://github.com/melodic-software/standards/blob/main/components/issue-body-contract/README.md#rules-the-contract-keeps) records why).

**Open issues conform; closed ones stay.** Retitle an open issue that breaks this rule, unless automation finds that issue by its exact title. Closed issues are not backfilled.

Expand All @@ -43,13 +43,13 @@ Every issue meets one rule, whether a person, an agent session or a bot workflow
- a parent issue when the work belongs to a larger one;
- a blocked-by edge when it waits on other work or a decision (see "The status axis" below).

The title and Issue Type are set at filing, always. The filer sets the other values it knows at filing; triage sets the rest before the issue is worked, and no value is ever a placeholder. A bot that files the same kind of record repeatedly (a post-merge alert, a monitoring incident) knows these values and sets them at filing. An issue tracks work with an owner, so a bot's current status belongs in its run's job summary; it files an issue only for an incident that needs action, one per incident.
The title and Issue Type are set at filing, always, except on an issue filed through the agent-ready work-item form, whose type triage sets. The filer sets the other values it knows at filing; triage sets the rest before the issue is worked, and no value is ever a placeholder. A bot that files the same kind of record repeatedly (a post-merge alert, a monitoring incident) knows these values and sets them at filing. An issue tracks work with an owner, so a bot's current status belongs in its run's job summary; it files an issue only for an incident that needs action, one per incident.

## The type axis

On organization repositories the type axis is **native GitHub Issue Types**, not labels. Issue Types are an organization-level, single-select field: an issue carries **at most one** type, never two. GitHub positions them as the successor to a type-prefixed label. They are the governance mechanism for this axis; there is no parallel type label alongside them. GitHub does not require every issue to carry a type: search exposes a positive `type:"Bug"`-style qualifier per enabled type but no `no:type` negation to find the untyped remainder directly (querying `no:type` silently returns the unfiltered total, not a filtered one), so finding what's missing means comparing per-type counts against the total or reading an issue's `type` field, which is `null` when unset. An issue created via a form or API call that skips the field can silently fall outside the taxonomy this way.

**Issue creation always sets the type.** CLI and agent-driven issue creation passes `--type` with one of the org's enabled native types, for example `gh issue create --type Task` (or `Bug`/`Feature`, whichever applies). The web UI's issue form surfaces the same Types field at creation time, but GitHub does not force it to be filled in there either; setting it is this policy's requirement, not something the platform enforces on either path. github-iac does not assign a type on a schedule; [ADR 0003](https://github.com/melodic-software/github-iac/blob/main/docs/adr/0003-issue-type-axis-native-issue-types.md) deleted that sweep. An issue that skips the field stays untyped until a person or the triage lane sets one of the live organization types.
**Issue creation always sets the type**, apart from the agent-ready work-item form above. CLI and agent-driven issue creation passes `--type` with one of the org's enabled native types, for example `gh issue create --type Task` (or `Bug`/`Feature`, whichever applies). The web UI's issue form surfaces the same Types field at creation time, but GitHub does not force it to be filled in there either; setting it is this policy's requirement, not something the platform enforces on either path. github-iac does not assign a type on a schedule; [ADR 0003](https://github.com/melodic-software/github-iac/blob/main/docs/adr/0003-issue-type-axis-native-issue-types.md) deleted that sweep. An issue that skips the field stays untyped until a person or the triage lane sets one of the live organization types.

Personal accounts have no native Issue Types, so their repositories retain a labeled type axis under the same naming grammar. This is a deliberate per-account divergence, not drift; each account's own infrastructure-as-code program is the record of which mechanism its repositories use (see "Definitions live in infrastructure-as-code" above).

Expand Down
Loading