Skip to content
Draft
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
19 changes: 19 additions & 0 deletions .agents/workflow-labels.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"labels": [
{"name": "type:bug", "color": "d73a4a", "description": "Issue describes a defect in intended behavior"},
{"name": "type:chore", "color": "6f42c1", "description": "Issue tracks maintenance or project infrastructure work"},
{"name": "type:feature", "color": "6f42c1", "description": "Issue proposes a new user-facing capability"},
{"name": "type:spike", "color": "6f42c1", "description": "Issue tracks a bounded investigation with a specific question"},
{"name": "type:support", "color": "6f42c1", "description": "Issue asks for usage help or support"},
{"name": "state:new", "color": "d4f4dd", "description": "New issue awaiting triage assessment"},
{"name": "state:validated", "color": "b4d9f5", "description": "Assessment complete; maintainer disposition is pending"},
{"name": "state:accepted", "color": "0e8a16", "description": "Maintainer decided to pursue this issue"},
{"name": "state:in-progress", "color": "1d76db", "description": "Authorized work is in progress"},
{"name": "state:in-review", "color": "c5def5", "description": "Plan or pull request awaits human review"},
{"name": "needs:info", "color": "f9d0c4", "description": "Needs specific information before the next decision"},
{"name": "needs:plan", "color": "fbca04", "description": "Needs an implementation plan or review of that plan"},
{"name": "needs:pr", "color": "fbca04", "description": "Needs a pull request or review of that pull request"},
{"name": "needs:spike", "color": "fbca04", "description": "Needs a maintainer-approved bounded investigation"},
{"name": "needs:rfc", "color": "fbca04", "description": "Needs a maintainer-directed RFC pull request"}
]
}
54 changes: 54 additions & 0 deletions docs/contributing/issue-workflow.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: "Issue Workflow"
description: "How OpenShell records an issue's kind, state, and next work."
---

OpenShell uses three label groups to show what an issue is, where it stands, and what work it needs next. The [workflow label manifest](https://github.com/NVIDIA/OpenShell/blob/main/.agents/workflow-labels.json) defines their names, descriptions, and colors.

## Read the Three Axes

| Axis | Meaning | Rule |
| --- | --- | --- |
| `type:*` | Kind of issue: bug, chore, feature, spike, or support. | At most one. It may be absent during intake; classify it from evidence before advancing the work. |
| `state:*` | Current assessment or work state. | Exactly one on an open issue once the intake workflow has run. |
| `needs:*` | The next missing work product or information. | At most one. It may be absent while a human makes a disposition or assignment decision. |

Use `state:new`, `state:validated`, `state:accepted`, `state:in-progress`, and `state:in-review` to describe the issue's current position. `needs:info`, `needs:plan`, `needs:pr`, `needs:spike`, and `needs:rfc` describe the next work. The issue discussion and assignment identify who should act.

GitHub's closed status and reason describe completed or declined work. Do not add terminal `state:done` or `state:canceled` labels. When closing a new issue, clear `needs:*` labels that no longer describe a next action. The migration leaves already closed items untouched.

GitHub's built-in issue type is separate metadata. Use `type:*` for the more specific workflow classification. Conventional Commit types describe commits and do not determine issue type. Usage questions may use `type:support`; suspected vulnerabilities belong in the private process in `SECURITY.md`, not a public `type:security` issue.

## Intake and Disposition

Non-maintainer issues start at `state:new`, which calls for initial screening only. For this rollout, a human invokes triage. The triage skill sends findings to `#openshell-triage` for the duty engineer and collaborators to discuss. The engineer directs any public response or label change. Aim to reach an explained acceptance or decline promptly; use `state:validated` only while a genuine maintainer decision is pending.

Maintainer-authored issues bypass triage for this rollout and enter `state:accepted` for scheduling and assignment. The separate automatic-triage follow-on will reconsider that exception. A maintainer still identifies the issue's type and next work when those are not evident from the submission.

Only a human decides whether to accept or decline work, authorizes a spike or RFC, and places an issue on the roadmap. An agent never infers acceptance from technical validity. A maintainer may record acceptance with `state:accepted` or roadmap placement. Maintainers direct an RFC and assign its number before applying `needs:rfc`. `needs:spike` represents an approved, bounded investigation, not a promise to implement its findings.

## Authorize Agent Work

For unattended planning or implementation, a human sets `needs:plan` or `needs:pr` after authorizing that phase. The agent checks acceptance or roadmap placement, a work state that permits pickup, the relevant `needs:*` label, and any required approved plan. `state:new` permits intake screening only; `state:in-review` means a human is reviewing a plan or PR. A request for a plan authorizes planning only; an implementation request follows plan approval. Agents do not set `needs:plan` or `needs:pr` to queue themselves.

A direct user request authorizes the phase it explicitly names even when the issue's labels are missing or incomplete. The agent names the discrepancy and continues without changing queue labels. General build agents do not handle `topic:security` issues; specialized security skills preserve the private review and human decision gates.

## Review Common Combinations

| Situation | Labels on the issue | Next action |
| --- | --- | --- |
| New community report | `state:new`; `type:*` optional pending assessment | Human invokes triage screening. |
| Needs an answer from the reporter | `state:new` or `state:validated`, `needs:info` | Ask a specific question and reassess the answer. |
| Validated, waiting for disposition | `type:bug` or `type:feature`, `state:validated` | Maintainer accepts or declines promptly. |
| Accepted, queued for a plan | `state:accepted`, `needs:plan` | Authorized agent prepares the plan. |
| Approved plan, queued for implementation | `state:accepted` or `state:in-progress`, `needs:pr` | Authorized agent prepares the PR. |
| Plan or PR ready for review | `state:in-review`, `needs:plan` or `needs:pr` | Human reviews and either approves or requests another pass. |
| Reopened after an earlier close | One current `state:*` and an appropriate `needs:*` if work is authorized | Reassess the new information and clear obsolete labels before work resumes. |

The following combinations need correction: two labels from one single-valued axis; `state:new` with `needs:pr`; `state:validated` with `needs:pr`; or `state:in-progress` with no known authorized next work. A missing `type:*` during intake is a classification task, not a reason to guess from the current label backlog. The migration rehearsal reports ambiguous historical combinations for human review.

## Pull Requests

Keep issue workflow labels on the linked issue. A pull request does not need to repeat them. If a pull request has no linked issue, labels from these axes are optional and must not be assumed to represent an accepted issue. Review and merge state remain visible in GitHub's pull request interface.
5 changes: 5 additions & 0 deletions docs/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ navigation:
title: "Observability"
- folder: kubernetes
title: "Kubernetes"
- section: "Contributing"
slug: contributing
contents:
- page: "Issue Workflow"
path: contributing/issue-workflow.mdx
- section: "Tutorials"
slug: tutorials
path: tutorials/index.mdx
Expand Down
115 changes: 115 additions & 0 deletions scripts/workflow_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Review and provision workflow label definitions without relabeling items."""

import argparse
import json
import re
import subprocess
from pathlib import Path

MANIFEST = Path(__file__).resolve().parents[1] / ".agents" / "workflow-labels.json"
AXES = ("type:", "state:", "needs:")


def load_manifest(path: Path) -> list[dict[str, str]]:
data = json.loads(path.read_text())
labels = data["labels"]
if not isinstance(labels, list):
raise ValueError("labels must be a list")

seen = set()
for label in labels:
name = label["name"]
if name in seen:
raise ValueError(f"duplicate label: {name}")
seen.add(name)
if not name.startswith(AXES):
raise ValueError(f"not a workflow-axis label: {name}")
if not re.fullmatch(r"[0-9a-fA-F]{6}", label["color"]):
raise ValueError(f"invalid color for {name}")
if not label["description"] or len(label["description"]) > 100:
raise ValueError(f"invalid description for {name}")
return labels


def plan_changes(
desired: list[dict[str, str]], existing: list[dict[str, str]]
) -> list[dict[str, str]]:
by_name = {label["name"]: label for label in existing}
changes = []
for label in desired:
current = by_name.get(label["name"])
if current is None:
action = "create"
elif (current["color"].lower(), current.get("description") or "") != (
label["color"].lower(),
label["description"],
):
action = "update"
else:
continue
changes.append({"action": action, **label})
return changes


def apply_changes(changes: list[dict[str, str]], repo: str, run) -> None:
for change in changes:
run(
[
"gh",
"label",
"create" if change["action"] == "create" else "edit",
change["name"],
"--repo",
repo,
"--color",
change["color"],
"--description",
change["description"],
]
)


def read_existing(repo: str) -> list[dict[str, str]]:
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repo):
raise ValueError("--repo must be OWNER/REPO")
result = subprocess.run(
["gh", "api", "--paginate", "--slurp", f"repos/{repo}/labels?per_page=100"],
check=True,
capture_output=True,
text=True,
)
return [label for page in json.loads(result.stdout) for label in page]


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--repo", default="NVIDIA/OpenShell", help="OWNER/REPO to inspect"
)
parser.add_argument("--manifest", type=Path, default=MANIFEST)
parser.add_argument(
"--apply", action="store_true", help="Create/update definitions after review"
)
args = parser.parse_args()

changes = plan_changes(load_manifest(args.manifest), read_existing(args.repo))
for change in changes:
print(
f"{change['action'].upper()} {change['name']}: {change['description']} ({change['color']})"
)
if not changes:
print("Workflow label definitions are current.")
if args.apply:

def run(command):
subprocess.run(command, check=True)

apply_changes(changes, args.repo, run)


if __name__ == "__main__":
main()
143 changes: 143 additions & 0 deletions scripts/workflow_labels_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Focused checks for repeatable workflow label provisioning."""

import json
import tempfile
import unittest
from pathlib import Path

try:
import workflow_labels
except ModuleNotFoundError:
workflow_labels = None


class WorkflowLabelsTest(unittest.TestCase):
def test_committed_manifest_is_valid(self):
self.assertIsNotNone(workflow_labels, "workflow_labels.py is missing")
manifest = (
Path(__file__).resolve().parents[1] / ".agents" / "workflow-labels.json"
)
labels = workflow_labels.load_manifest(manifest)
self.assertIn("state:new", {label["name"] for label in labels})
self.assertFalse(any(label["name"].startswith("ready-for:") for label in labels))

def test_manifest_rejects_duplicate_names(self):
self.assertIsNotNone(workflow_labels, "workflow_labels.py is missing")
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "labels.json"
path.write_text(
json.dumps(
{
"labels": [
{
"name": "state:new",
"color": "ffffff",
"description": "New",
},
{
"name": "state:new",
"color": "ffffff",
"description": "Duplicate",
},
]
}
)
)
with self.assertRaisesRegex(ValueError, "duplicate.*state:new"):
workflow_labels.load_manifest(path)

def test_plan_only_changes_managed_definitions(self):
self.assertIsNotNone(workflow_labels, "workflow_labels.py is missing")
desired = [
{"name": "state:new", "color": "d4f4dd", "description": "New submission"},
{"name": "needs:plan", "color": "f2c97d", "description": "Needs a plan"},
]
existing = [
{"name": "state:new", "color": "ffffff", "description": "Old description"},
{"name": "roadmap", "color": "123456", "description": "Unrelated"},
]

changes = workflow_labels.plan_changes(desired, existing)

self.assertEqual(
changes,
[
{
"action": "update",
"name": "state:new",
"color": "d4f4dd",
"description": "New submission",
},
{
"action": "create",
"name": "needs:plan",
"color": "f2c97d",
"description": "Needs a plan",
},
],
)
provisioned = [*existing[1:], *desired]
self.assertEqual(workflow_labels.plan_changes(desired, provisioned), [])

def test_apply_changes_only_edits_label_definitions(self):
self.assertIsNotNone(workflow_labels, "workflow_labels.py is missing")
calls = []

def run(args):
calls.append(args)

workflow_labels.apply_changes(
[
{
"action": "create",
"name": "state:new",
"color": "d4f4dd",
"description": "New submission",
},
{
"action": "update",
"name": "needs:plan",
"color": "f2c97d",
"description": "Needs a plan",
},
],
"NVIDIA/OpenShell",
run,
)

self.assertEqual(
calls,
[
[
"gh",
"label",
"create",
"state:new",
"--repo",
"NVIDIA/OpenShell",
"--color",
"d4f4dd",
"--description",
"New submission",
],
[
"gh",
"label",
"edit",
"needs:plan",
"--repo",
"NVIDIA/OpenShell",
"--color",
"f2c97d",
"--description",
"Needs a plan",
],
],
)


if __name__ == "__main__":
unittest.main()
Loading