diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d7f48b6b..6e1523a7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -33,7 +33,7 @@ { "name": "darrow-tickets", "source": "./plugins/capability/darrow-tickets", - "description": "Ticket skills with a bundled GitHub Issues adapter: create-ticket, read-ticket, update-ticket, list-tickets" + "description": "Ticket operations and reusable GitHub issue templates with a bundled GitHub Issues adapter" }, { "name": "darrow-readiness-gate", diff --git a/.github/ISSUE_TEMPLATE/bug-fix-regression.md b/.github/ISSUE_TEMPLATE/bug-fix-regression.md new file mode 100644 index 00000000..8c8dc2bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-fix-regression.md @@ -0,0 +1,32 @@ +--- +name: Bug fix with regression coverage +about: Fix a reproduced bug and retain evidence that it stays fixed +--- + +## Observed + + + +## Expected + + + +## Reproduction + + + +## Done criteria + +- [ ] Demonstrate the original failure with the supplied reproducer or a regression test. +- [ ] Make a bounded fix for that failure. +- [ ] Run the same reproducer or regression test after the fix. +- [ ] Retain durable regression coverage and before/after evidence. +- [ ] Report insufficient reproduction evidence as a blocker to verified repair. + +## Before and after evidence + + + +## Open questions + + diff --git a/.github/ISSUE_TEMPLATE/dependency-upgrade.md b/.github/ISSUE_TEMPLATE/dependency-upgrade.md new file mode 100644 index 00000000..cdcec11b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/dependency-upgrade.md @@ -0,0 +1,23 @@ +--- +name: Dependency upgrade +about: Upgrade a dependency with evidence that affected behavior remains compatible +--- + +## Outcome + + + +## Done criteria + +- [ ] Inspect the relevant changelog or release notes for the requested update. +- [ ] Identify repository-relevant breaking changes and migration requirements. +- [ ] Update the dependency and affected code or configuration. +- [ ] Verify behavior affected by the upgrade and record compatibility evidence. + +## Compatibility evidence + + + +## Open questions + + diff --git a/.github/ISSUE_TEMPLATE/retirement.md b/.github/ISSUE_TEMPLATE/retirement.md new file mode 100644 index 00000000..e7691793 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/retirement.md @@ -0,0 +1,23 @@ +--- +name: Retirement +about: Retire a named behavior or dependency without leaving hidden obligations +--- + +## Outcome + + + +## Done criteria + +- [ ] Remove the authorized implementation and configuration paths. +- [ ] Update or remove affected tests and documentation. +- [ ] Address migration obligations for users or callers. +- [ ] Show that removal is complete and retained behavior still functions. + +## Remaining obligations or blockers + + + +## Open questions + + diff --git a/docs/choosing-plugins.md b/docs/choosing-plugins.md index c36a7a49..4d421193 100644 --- a/docs/choosing-plugins.md +++ b/docs/choosing-plugins.md @@ -15,6 +15,7 @@ alone; its README owns its prerequisites, behavior, and safety rules. | Verify implementation acceptance through independent assessment | [Verification](../plugins/capability/darrow-verification/README.md) | | Create a branch, commit, or pull request | [Git](../plugins/capability/darrow-git/README.md) | | Read, list, create, or update GitHub issues | [Tickets](../plugins/capability/darrow-tickets/README.md) | +| Install reusable GitHub issue templates | [Tickets](../plugins/capability/darrow-tickets/README.md#install-ticket-templates) | | Understand technical structure in a compact visual | [Explanation](../plugins/capability/darrow-explanation/README.md) | | Export Codex turn telemetry to Langfuse | [Langfuse observability](../plugins/capability/darrow-observability-langfuse/README.md) | | Organize repository agent guidance | [Information architecture](../plugins/foundation/darrow-information-architecture/README.md) | diff --git a/docs/specs/ticket-management.md b/docs/specs/ticket-management.md index ac9a15af..9f114331 100644 --- a/docs/specs/ticket-management.md +++ b/docs/specs/ticket-management.md @@ -5,8 +5,9 @@ creating and updating tickets is consistent, traceable, and backend-neutral regardless of which agent runtime executes them and which tracker backs them. Plugin: `darrow-tickets`. Skills: `create-ticket`, `read-ticket`, -`update-ticket`, `list-tickets`. The bundled GitHub Issues provider is the -only shipped tracker adapter. Other trackers may be added inside this plugin. +`update-ticket`, `list-tickets`, and `install-ticket-templates`. The bundled +GitHub Issues provider is the only shipped tracker adapter. Other trackers may +be added inside this plugin. Scope: mechanics only. These skills record and mutate tickets; they do not refine requirements, plan or break down work, or review solutions. Those @@ -47,7 +48,7 @@ stable intent ("create a ticket for X") while the backend stays swappable. remain static and nonmutating, perform no repository or tracker inspection, preserve provider boundaries, and leave workflow and authority in the skill. -- **TM-P1 — One self-contained ticket plugin.** The four skills and all tracker +- **TM-P1 — One self-contained ticket plugin.** The five skills and all tracker adapters live in `darrow-tickets`. The shipped adapter is GitHub Issues via `gh`. Future trackers extend this contained Python package; no sibling plugin, shared runtime, or external provider registry is required. Only the selected @@ -76,6 +77,50 @@ stable intent ("create a ticket for X") while the backend stays swappable. bound to the resolved origin host and repository independently of ambient `GH_HOST` or `GH_REPO` configuration. +## GitHub issue templates + +The plugin bundles three GitHub Markdown issue templates. Installation is a +separate, explicitly requested local repository operation; it does not create a +ticket or authorize delivery. The templates use the ordinary `create-ticket` +body headings so both human-authored and agent-created issues can carry the +same task requirements. + +- **TM-T1 — Task-specific requirements.** The dependency-upgrade template asks + for the dependency and target version, relevant changelog or release notes, + repository-relevant breaking changes and migration work, and compatibility + evidence for affected behavior. The retirement template asks for the exact + removal target and authorized scope, covers implementation, configuration, + tests, documentation and migration obligations, and calls for evidence that + removal is complete and retained behavior works. The bug-fix template asks + for incident evidence, observed and expected behavior, a reproducer, a + bounded fix, durable regression coverage and before/after evidence. Missing + reproduction evidence remains an explicit blocker to a verified repair. +- **TM-T2 — Additive installation.** `install-ticket-templates` installs the + bundled templates into the named repository's `.github/ISSUE_TEMPLATE/`. + It creates absent files, leaves identical files unchanged, and preserves + differing files, symlinks, directories, and unrelated templates without + overwriting them. It reports absolute paths and each created, unchanged or + preserved result. An unreadable or unusable target, including a case-variant + filename that conflicts with a bundled path, refuses the operation. + Installing templates changes local files only; it does not create issues, + commit, push, or start `ticket-to-pr`. +- **TM-T3 — Installed template is authoritative.** `create-ticket` discovers + the current repository's GitHub Markdown templates and YAML issue forms, + including files supplied or customized by its users. It inspects an explicitly + named template, or selects one whose stated purpose matches the requested + issue; ambiguous matches require a caller choice. The created ticket retains + the selected template's applicable criteria and evidence requests. It never + substitutes the bundled copy for a missing or unreadable installed template. + Without a matching installed template, the existing type-based drafting rules + apply. Factual inputs come only from the request, conversation or repository; + missing versions, removal scope, reproduction details and other facts remain + explicit rather than invented. Template inspection does not create a ticket + or start delivery. +- **TM-T4 — Same-path adoption.** Darrow installs the bundled templates with + the same public installation operation offered to other repositories. The + resulting checked-in templates are ordinary repository files, not a plugin + runtime dependency or a separate ticket store. + ## Relations contract Tickets relate in two ways: `depends-on` (this ticket is blocked by @@ -174,6 +219,15 @@ decompose a broad request into tickets on its own. created tickets remain visible, and no automatic rollback or second creation attempt occurs. A skipped item does not prevent independent items from proceeding. Never substitute a missing batch relation target. +- **TM-C11 — Installed template use.** After duplicate checking and before + drafting each GitHub issue, inspect available repository templates and any + requested or matching template. Retain its applicable criteria, evidence + requests, and local customizations in the created body while satisfying the + chosen ticket type's required headings. A missing template uses TM-C3; an + unreadable or unsafe installed template stops that item before creation. + Ordinary bug reports without a requested fix need not select the bundled + regression-fix template. Unknown factual inputs remain open questions, + never guessed values or claimed evidence. ### Non-goals diff --git a/plugins/capability/darrow-tickets/.claude-plugin/plugin.json b/plugins/capability/darrow-tickets/.claude-plugin/plugin.json index 509e258f..e9b72d50 100644 --- a/plugins/capability/darrow-tickets/.claude-plugin/plugin.json +++ b/plugins/capability/darrow-tickets/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "darrow-tickets", - "description": "Ticket skills with a bundled GitHub Issues adapter: create-ticket, read-ticket, update-ticket, list-tickets", - "version": "0.6.4", + "description": "Ticket operations and reusable GitHub issue templates with a bundled GitHub Issues adapter", + "version": "0.7.0", "hooks": "./.claude-plugin/hooks.json", "license": "BUSL-1.1", "author": { diff --git a/plugins/capability/darrow-tickets/.codex-plugin/plugin.json b/plugins/capability/darrow-tickets/.codex-plugin/plugin.json index 4b5da53a..2f2b8499 100644 --- a/plugins/capability/darrow-tickets/.codex-plugin/plugin.json +++ b/plugins/capability/darrow-tickets/.codex-plugin/plugin.json @@ -1,15 +1,15 @@ { "name": "darrow-tickets", - "version": "0.6.4", - "description": "Ticket skills with a bundled GitHub Issues adapter: create-ticket, read-ticket, update-ticket, list-tickets", + "version": "0.7.0", + "description": "Ticket operations and reusable GitHub issue templates with a bundled GitHub Issues adapter", "author": { "name": "Björn Rochel" }, "skills": "./skills/", "interface": { "displayName": "Darrow -> Tickets", - "shortDescription": "Create, read, find, and update tickets", - "longDescription": "Create one well-formed ticket or an explicit finite batch, retrieve one exact ticket, find relevant open work, and apply one verified update through the bundled GitHub Issues adapter.", + "shortDescription": "Manage tickets and install issue templates", + "longDescription": "Create, read, find, and update GitHub tickets, and install reusable issue templates for dependency upgrades, retirement, and regression fixes.", "developerName": "Björn Rochel", "category": "Productivity", "capabilities": ["Interactive", "Read", "Write"], @@ -17,7 +17,8 @@ "Create a ticket for this problem.", "Read ticket #42.", "List the open bugs for this project.", - "Close the ticket for this completed work." + "Close the ticket for this completed work.", + "Install Darrow's GitHub issue templates in this repository." ] } } diff --git a/plugins/capability/darrow-tickets/README.md b/plugins/capability/darrow-tickets/README.md index 71ab6906..a2de8aa2 100644 --- a/plugins/capability/darrow-tickets/README.md +++ b/plugins/capability/darrow-tickets/README.md @@ -1,8 +1,9 @@ # Darrow Tickets -This independently installable plugin provides everyday ticket operations. Four -skills decide what the user means and how to present evidence; one contained -Python package selects a tracker adapter and performs the requested operation. +This independently installable plugin provides everyday ticket operations and +reusable GitHub issue templates. Five skills decide what the user means and how +to present evidence; one contained Python package supplies the tracker adapter +and local template operations. GitHub Issues through `gh` is the only bundled adapter in this release. The operation skills and CLI shape remain stable as adapters are added inside @@ -31,9 +32,21 @@ distinct tickets. It checks each item for a plausible duplicate, selects its type, uses the tracker's existing label taxonomy, structures its body, preserves unknowns as open questions, and records only caller-named relations. It verifies requested relations after creation and reports every batch outcome. +For a matching issue, it reads an installed Markdown template or YAML issue +form, including user-defined templates and local customizations, and retains +its acceptance and evidence requirements. Example: _“File a bug for the failing CSV import.”_ +### `install-ticket-templates` + +Adds three GitHub Markdown templates to a target repository's +`.github/ISSUE_TEMPLATE/`: dependency upgrade, retirement, and bug fix with +regression coverage. It preserves existing files and customizations, reports +each resulting path, and does not create an issue or start delivery. + +Example: _“Install Darrow's issue templates in this repository.”_ + ### `list-tickets` Runs one read-only query using only the state, type, label, milestone, topic, @@ -61,7 +74,7 @@ Example: _“Comment on #42 with the failing command.”_ ### `darrow-ticket` -A contained Python facade used by all four skills. Its public argument parser +A contained Python facade used by the four ticket-operation skills. Its public argument parser preserves provider-owned identifiers, selects one bundled adapter, and then dispatches one operation. Shared validation covers ticket body structure and attribution. The GitHub adapter owns numeric IDs, repository resolution, `gh` @@ -95,6 +108,22 @@ Exit codes: 2 input/filesystem error, 3 unusable backend, 4 provider failure, 5 invalid title, 6 attribution, 7 body structure, 8 label error, 9 state refusal, 64 unknown or missing command. +### `darrow-ticket-templates` + +The same contained package exposes local template commands, without tracker +access: + +```text +uv run --quiet --no-project "/backend/scripts/run_locked.py" darrow-ticket-templates install --repo +uv run --quiet --no-project "/backend/scripts/run_locked.py" darrow-ticket-templates list --repo +uv run --quiet --no-project "/backend/scripts/run_locked.py" darrow-ticket-templates show --repo --file +``` + +`install` creates absent bundled files and leaves identical, modified, and +unrelated files alone. `list` and `show` read the repository's installed +Markdown templates and YAML forms, including user-defined ones. `create-ticket` +does not silently use a bundled default when a repository has not adopted it. + ## Design boundaries - `create-ticket` may create an explicitly requested finite batch of distinct @@ -105,6 +134,8 @@ Exit codes: 2 input/filesystem error, 3 unusable backend, 4 provider failure, milestone, or assignee. - Ticket content contains repository or user evidence, never invented versions, reproduction steps, acceptance criteria, or AI attribution. +- Matching installed templates supply task-specific acceptance criteria, not + factual values or permission to start the work in a ticket. - `read-ticket` and `list-tickets` are strictly read-only, and `update-ticket` applies only the single mutation requested. - A selected adapter is the only tracker contacted. Adding another adapter must @@ -167,7 +198,8 @@ or use `/darrow-tickets:read-ticket` in Claude Code, followed by your request. ## Expected result Read and list return tracker evidence without changes. Create reports each -requested ticket outcome; update performs one requested mutation. +requested ticket outcome; update performs one requested mutation. Template +installation reports local file changes without contacting GitHub. ## Troubleshooting diff --git a/plugins/capability/darrow-tickets/backend/pyproject.toml b/plugins/capability/darrow-tickets/backend/pyproject.toml index 4134881e..5956c7ef 100644 --- a/plugins/capability/darrow-tickets/backend/pyproject.toml +++ b/plugins/capability/darrow-tickets/backend/pyproject.toml @@ -7,6 +7,7 @@ dependencies = [] [project.scripts] darrow-ticket = "darrow_tickets.cli:entrypoint" +darrow-ticket-templates = "darrow_tickets.templates:entrypoint" darrow-tickets-claude-context = "darrow_tickets.claude_context:main" [dependency-groups] diff --git a/plugins/capability/darrow-tickets/backend/src/darrow_tickets/claude_routing.md b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/claude_routing.md index 7adcb353..4d9c5f69 100644 --- a/plugins/capability/darrow-tickets/backend/src/darrow_tickets/claude_routing.md +++ b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/claude_routing.md @@ -1,5 +1,7 @@ The installed Darrow ticket skills own requests to create, list, read, or update -tickets through a bundled tracker adapter. GitHub Issues is currently the only +tickets through a bundled tracker adapter. The install-ticket-templates skill +owns requests to add the bundled GitHub issue templates to a repository. +GitHub Issues is currently the only bundled tracker. When it is selected or no tracker is established, invoke the matching installed skill before repository inspection, tracker access, clarification, or your final response. Select from the installed descriptions; diff --git a/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/bug-fix-regression.md b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/bug-fix-regression.md new file mode 100644 index 00000000..8c8dc2bc --- /dev/null +++ b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/bug-fix-regression.md @@ -0,0 +1,32 @@ +--- +name: Bug fix with regression coverage +about: Fix a reproduced bug and retain evidence that it stays fixed +--- + +## Observed + + + +## Expected + + + +## Reproduction + + + +## Done criteria + +- [ ] Demonstrate the original failure with the supplied reproducer or a regression test. +- [ ] Make a bounded fix for that failure. +- [ ] Run the same reproducer or regression test after the fix. +- [ ] Retain durable regression coverage and before/after evidence. +- [ ] Report insufficient reproduction evidence as a blocker to verified repair. + +## Before and after evidence + + + +## Open questions + + diff --git a/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/dependency-upgrade.md b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/dependency-upgrade.md new file mode 100644 index 00000000..cdcec11b --- /dev/null +++ b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/dependency-upgrade.md @@ -0,0 +1,23 @@ +--- +name: Dependency upgrade +about: Upgrade a dependency with evidence that affected behavior remains compatible +--- + +## Outcome + + + +## Done criteria + +- [ ] Inspect the relevant changelog or release notes for the requested update. +- [ ] Identify repository-relevant breaking changes and migration requirements. +- [ ] Update the dependency and affected code or configuration. +- [ ] Verify behavior affected by the upgrade and record compatibility evidence. + +## Compatibility evidence + + + +## Open questions + + diff --git a/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/retirement.md b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/retirement.md new file mode 100644 index 00000000..e7691793 --- /dev/null +++ b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/template_assets/retirement.md @@ -0,0 +1,23 @@ +--- +name: Retirement +about: Retire a named behavior or dependency without leaving hidden obligations +--- + +## Outcome + + + +## Done criteria + +- [ ] Remove the authorized implementation and configuration paths. +- [ ] Update or remove affected tests and documentation. +- [ ] Address migration obligations for users or callers. +- [ ] Show that removal is complete and retained behavior still functions. + +## Remaining obligations or blockers + + + +## Open questions + + diff --git a/plugins/capability/darrow-tickets/backend/src/darrow_tickets/templates.py b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/templates.py new file mode 100644 index 00000000..6d2bd0b3 --- /dev/null +++ b/plugins/capability/darrow-tickets/backend/src/darrow_tickets/templates.py @@ -0,0 +1,191 @@ +"""Install and inspect repository-owned GitHub issue templates.""" + +import argparse +import os +import re +import subprocess +import sys +from collections.abc import Sequence +from pathlib import Path + +from .errors import TicketError + +KINDS = ("dependency-upgrade", "retirement", "bug-fix-regression") +ASSETS = Path(__file__).parent / "template_assets" +EXTENSIONS = {".md", ".yml", ".yaml"} + + +def repository_root(value: str) -> Path: + path = Path(value).expanduser().resolve() + if not path.is_dir(): + raise TicketError(f"error: repository is not a directory: {path}") + environment = { + key: item + for key, item in os.environ.items() + if not key.upper().startswith("GIT_") + } + result = subprocess.run( + ["git", "-C", str(path), "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + encoding="utf-8", + check=False, + env=environment, + ) + if result.returncode or Path(result.stdout.strip()).resolve() != path: + raise TicketError(f"error: target is not a repository root: {path}") + return path + + +def template_directory(root: Path, *, create: bool) -> Path: + github = root / ".github" + target = github / "ISSUE_TEMPLATE" + for directory in (github, target): + if directory.is_symlink(): + raise TicketError(f"error: template directory is a symlink: {directory}") + if directory.exists() and not directory.is_dir(): + raise TicketError( + f"error: template directory is not a directory: {directory}" + ) + if create: + target.mkdir(parents=True, exist_ok=True) + return target + + +def valid_filename(filename: str) -> bool: + return ( + filename.casefold() not in {".", "..", "config.yml", "config.yaml"} + and Path(filename).name == filename + and "/" not in filename + and "\\" not in filename + and Path(filename).suffix.lower() in EXTENSIONS + ) + + +def template_path(root: Path, filename: str) -> Path: + if not valid_filename(filename): + raise TicketError(f"error: invalid template filename: {filename}") + return template_directory(root, create=False) / filename + + +def bundled_templates() -> dict[str, bytes]: + return {kind: (ASSETS / f"{kind}.md").read_bytes() for kind in KINDS} + + +def install_status(path: Path, expected: bytes) -> str: + if path.is_symlink() or path.is_dir(): + return "preserved" + if path.exists(): + return "unchanged" if path.read_bytes() == expected else "preserved" + return "created" + + +def install_targets(directory: Path, bundle: dict[str, bytes]) -> dict[str, Path]: + names = {f"{kind}.md" for kind in bundle} + if directory.exists(): + for path in directory.iterdir(): + key = path.name.casefold() + if key in names and path.name != key: + raise TicketError( + f"error: case-variant template blocks installation: {path}" + ) + return {kind: directory / f"{kind}.md" for kind in bundle} + + +def install(root: Path) -> None: + target_dir = template_directory(root, create=False) + bundle = bundled_templates() + paths = install_targets(target_dir, bundle) + statuses: dict[str, str] = {} + for kind, expected in bundle.items(): + path = paths[kind] + statuses[kind] = install_status(path, expected) + template_directory(root, create=True) + for kind, status in statuses.items(): + path = paths[kind] + if status == "created": + with path.open("xb") as stream: + stream.write(bundle[kind]) + print(f"{status}: {path}") + + +def metadata(content: str) -> dict[str, str]: + fields: dict[str, str] = {} + for key, value in re.findall( + r"^(name|about|description):[ \t]*(.+)$", content, re.M + ): + if key not in fields: + fields[key] = value.strip().strip("\"'") + return fields + + +def print_template_entry(path: Path) -> None: + if path.is_symlink() or not path.is_file(): + raise TicketError(f"error: installed template is not a regular file: {path}") + fields = metadata(path.read_text(encoding="utf-8")) + print(f"file: {path.name}\npath: {path}") + for key in ("name", "about", "description"): + if key in fields: + print(f"{key}: {fields[key]}") + + +def list_templates(root: Path) -> None: + directory = template_directory(root, create=False) + if not directory.exists(): + print(f"status: missing\npath: {directory}") + return + paths = sorted( + (path for path in directory.iterdir() if valid_filename(path.name)), + key=lambda path: path.name.casefold(), + ) + print(f"status: {'available' if paths else 'empty'}\npath: {directory}") + for path in paths: + print_template_entry(path) + + +def show(root: Path, filename: str) -> None: + path = template_path(root, filename) + if path.is_symlink() or path.is_dir(): + raise TicketError(f"error: installed template is not a regular file: {path}") + if not path.exists(): + print(f"status: missing\npath: {path}") + return + content = path.read_text(encoding="utf-8") + print(f"status: installed\npath: {path}\ncontent:\n{content}", end="") + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser(prog="darrow-ticket-templates") + commands = result.add_subparsers(dest="command", required=True) + install_parser = commands.add_parser("install") + install_parser.add_argument("--repo", required=True) + list_parser = commands.add_parser("list") + list_parser.add_argument("--repo", required=True) + show_parser = commands.add_parser("show") + show_parser.add_argument("--repo", required=True) + show_parser.add_argument("--file", required=True) + return result + + +def main(argv: Sequence[str] | None = None) -> int: + try: + args = parser().parse_args(argv) + root = repository_root(args.repo) + if args.command == "install": + install(root) + elif args.command == "list": + list_templates(root) + else: + show(root, args.file) + except (OSError, UnicodeError, TicketError) as exc: + message = str(exc) + print( + message if message.startswith("error:") else f"error: {message}", + file=sys.stderr, + ) + return 2 + return 0 + + +def entrypoint() -> None: + raise SystemExit(main()) diff --git a/plugins/capability/darrow-tickets/backend/tests/fresh_install.py b/plugins/capability/darrow-tickets/backend/tests/fresh_install.py index 9076116c..208ac5bd 100644 --- a/plugins/capability/darrow-tickets/backend/tests/fresh_install.py +++ b/plugins/capability/darrow-tickets/backend/tests/fresh_install.py @@ -60,6 +60,30 @@ def validate(copy: Path, fixture: Path) -> None: repo.mkdir() command(repo, "git", "init", "-q") command(repo, "git", "remote", "add", "origin", "https://github.test/o/r.git") + installed = command( + repo, *uv, "darrow-ticket-templates", "install", "--repo", str(repo) + ) + assert installed.count("created:") == 3 + templates = repo / ".github" / "ISSUE_TEMPLATE" + assert ( + "name: Dependency upgrade" in (templates / "dependency-upgrade.md").read_text() + ) + (templates / "custom.yml").write_text( + "name: Custom issue\ndescription: Local concern\nbody: []\n" + ) + listed = command(repo, *uv, "darrow-ticket-templates", "list", "--repo", str(repo)) + assert "custom.yml" in listed and "Custom issue" in listed + shown = command( + repo, + *uv, + "darrow-ticket-templates", + "show", + "--repo", + str(repo), + "--file", + "custom.yml", + ) + assert "description: Local concern" in shown output = command(repo, *uv, "python", str(backend / "tests" / "fresh_probe.py")) assert "all eleven commands passed" in output diff --git a/plugins/capability/darrow-tickets/backend/tests/test_templates.py b/plugins/capability/darrow-tickets/backend/tests/test_templates.py new file mode 100644 index 00000000..869b9732 --- /dev/null +++ b/plugins/capability/darrow-tickets/backend/tests/test_templates.py @@ -0,0 +1,139 @@ +"""Installed issue templates are additive and remain repository-owned.""" + +import subprocess +from pathlib import Path + +import pytest + +from darrow_tickets.templates import main + + +def repository(root: Path) -> Path: + root.mkdir() + subprocess.run(["git", "init", "-q", str(root)], check=True) + return root + + +def test_install_and_inspect_all_three_templates( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root = repository(tmp_path / "repo with spaces") + target = root / ".github" / "ISSUE_TEMPLATE" + target.mkdir(parents=True) + (target / "config.yml").write_text("blank_issues_enabled: false\n") + + assert main(["install", "--repo", str(root)]) == 0 + output = capsys.readouterr().out + for kind in ("dependency-upgrade", "retirement", "bug-fix-regression"): + path = target / f"{kind}.md" + assert path.is_file() + assert f"created: {path}" in output + assert "name:" in path.read_text() + assert "about:" in path.read_text() + assert main(["show", "--repo", str(root), "--file", f"{kind}.md"]) == 0 + shown = capsys.readouterr().out + assert f"path: {path}" in shown + assert "status: installed" in shown + assert path.read_text() in shown + assert (target / "config.yml").read_text() == "blank_issues_enabled: false\n" + + assert main(["install", "--repo", str(root)]) == 0 + second = capsys.readouterr().out + assert second.count("unchanged:") == 3 + + +def test_customization_is_preserved_and_shown( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root = repository(tmp_path / "repo") + target = root / ".github" / "ISSUE_TEMPLATE" + target.mkdir(parents=True) + customized = target / "dependency-upgrade.md" + customized.write_text("local compatibility check\n") + + assert main(["install", "--repo", str(root)]) == 0 + assert f"preserved: {customized}" in capsys.readouterr().out + assert customized.read_text() == "local compatibility check\n" + assert main(["show", "--repo", str(root), "--file", "dependency-upgrade.md"]) == 0 + assert "local compatibility check" in capsys.readouterr().out + + +def test_install_refuses_case_variant_without_partial_installation( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root = repository(tmp_path / "repo") + target = root / ".github" / "ISSUE_TEMPLATE" + target.mkdir(parents=True) + existing = target / "Dependency-Upgrade.MD" + existing.write_text("custom upgrade\n") + + assert main(["install", "--repo", str(root)]) == 2 + output = capsys.readouterr() + assert not output.out + assert f"case-variant template blocks installation: {existing}" in output.err + assert existing.read_text() == "custom upgrade\n" + assert not any(path.name == "dependency-upgrade.md" for path in target.iterdir()) + assert not (target / "retirement.md").exists() + + +def test_missing_installed_template_never_falls_back_to_bundle( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root = repository(tmp_path / "repo") + assert main(["show", "--repo", str(root), "--file", "retirement.md"]) == 0 + output = capsys.readouterr().out + assert "status: missing" in output + assert "name:" not in output + assert not (root / ".github").exists() + + +def test_user_defined_markdown_and_yaml_forms_are_discoverable( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root = repository(tmp_path / "repo") + target = root / ".github" / "ISSUE_TEMPLATE" + target.mkdir(parents=True) + markdown = target / "security-hardening.md" + markdown.write_text( + "---\nname: Security hardening\nabout: Reduce exposed surface\n---\n" + ) + form = target / "upgrade-service.yml" + form.write_text( + "name: Service upgrade\ndescription: Update a service safely\nbody: []\n" + ) + (target / "config.yml").write_text("blank_issues_enabled: false\n") + + assert main(["list", "--repo", str(root)]) == 0 + listing = capsys.readouterr().out + assert "security-hardening.md" in listing + assert "Security hardening" in listing + assert "upgrade-service.yml" in listing + assert "Service upgrade" in listing + assert "config.yml" not in listing + assert main(["show", "--repo", str(root), "--file", "upgrade-service.yml"]) == 0 + assert form.read_text() in capsys.readouterr().out + + +def test_show_rejects_path_traversal( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + root = repository(tmp_path / "repo") + assert main(["show", "--repo", str(root), "--file", "../secret.md"]) == 2 + assert "filename" in capsys.readouterr().err + + +def test_install_refuses_non_repository_and_symlink_directory( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + ordinary = tmp_path / "ordinary" + ordinary.mkdir() + assert main(["install", "--repo", str(ordinary)]) == 2 + assert "repository" in capsys.readouterr().err + + root = repository(tmp_path / "repo") + outside = tmp_path / "outside" + outside.mkdir() + (root / ".github").symlink_to(outside, target_is_directory=True) + assert main(["install", "--repo", str(root)]) == 2 + assert "symlink" in capsys.readouterr().err + assert not list(outside.iterdir()) diff --git a/plugins/capability/darrow-tickets/skills/create-ticket/SKILL.md b/plugins/capability/darrow-tickets/skills/create-ticket/SKILL.md index ed33e2a1..cd5d36d4 100644 --- a/plugins/capability/darrow-tickets/skills/create-ticket/SKILL.md +++ b/plugins/capability/darrow-tickets/skills/create-ticket/SKILL.md @@ -21,6 +21,9 @@ review, or start the work. - **Evidence-grounded body:** facts come from the request, conversation, or repository. Unknowns stay explicit questions; they never become invented reproduction steps, versions, paths, or acceptance criteria. +- **Installed template:** use a matching GitHub issue template from the current + repository, whether it came from Darrow or its users. Preserve its criteria + and customizations while grounding every factual field in evidence. - **Caller-owned structure:** dependency, parent, milestone, and assignee choices are recorded only when the user names them. The caller may delegate the order of an already defined batch; that does not authorize new tickets @@ -136,6 +139,42 @@ For each item that passed its duplicate check, write a concise searchable title that states its problem or desired outcome, not a speculative implementation. Keep it one line with no trailing period. +When GitHub is the selected tracker, list the current repository's installed +issue templates before drafting. The command lists Markdown templates and YAML +issue forms with their absolute paths and available names and descriptions: + +```sh +uv run --quiet --no-project "/../../backend/scripts/run_locked.py" darrow-ticket-templates list --repo +``` + +Use the template the caller named, or one whose stated purpose clearly matches +the requested ticket. The bundled names cover dependency upgrades, retirement, +and bug fixes with regression coverage; user-defined templates can cover any +other purpose. If several plausibly match, ask which one before creation. If +none matches or the list reports `missing` or `empty`, use the ordinary type +structure below. Do not load a bundled copy instead of an installed template. +An ordinary bug report without a requested fix need not use the bundled +regression-fix template. + +For a selected filename from that list, inspect its installed content: + +```sh +uv run --quiet --no-project "/../../backend/scripts/run_locked.py" darrow-ticket-templates show --repo --file +``` + +These local commands do not contact GitHub. Use applicable headings, prompts, +acceptance criteria and evidence requests from the exact installed Markdown +template or YAML form. Express a YAML form's requirements as a Markdown issue +body rather than copying its form syntax. Remove frontmatter, HTML prompts, +and blank placeholders from the drafted body. Keep the required type headings +below, adding them if the template uses different headings. An unreadable or +unsafe installed template stops that item's creation. + +Before creation, compare the drafted body with every applicable template +criterion and evidence request. Keep each requirement as a checkable ticket +condition or name why it cannot yet be stated. A request for evidence is a +future acceptance condition, not a claim that the work has already passed. + Use exactly the required body structure for the chosen type: | Type | Required sections | @@ -160,7 +199,8 @@ requested. Do not derive secondary requirements such as pagination, filtering, naming, performance, rollout, or compatibility merely because an implementation might -need to consider them. Open questions are not discovery prompts: include one +need to consider them; an installed matching template may supply explicit +criteria for its own task. Open questions are not discovery prompts: include one only when the caller explicitly identified an uncertainty, supplied sources conflict, or a fact required to truthfully populate a mandatory section is missing. An unstated optional behavior or possible design choice stays omitted. diff --git a/plugins/capability/darrow-tickets/skills/create-ticket/evals/bug-regression-template.yaml b/plugins/capability/darrow-tickets/skills/create-ticket/evals/bug-regression-template.yaml new file mode 100644 index 00000000..a050a539 --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/create-ticket/evals/bug-regression-template.yaml @@ -0,0 +1,40 @@ +id: create-ticket-bug-regression-template +invariant: TM-T1,TM-T3,TM-C4,TM-C11 +activation: competition +mount_plugin_skills: true +prompt: >- + File a bug-fix ticket with regression coverage. `bun test csv-import + --case empty-header` crashes with `TypeError: missing header`. It should + report a validation error. Require the same case to fail before the fix and + pass afterward, and keep a regression test. We do not yet know which commit + introduced it. +fixture: + commits: + - message: "chore: init" + files: + README.md: "# Fixture\n" + bin: + gh: | + #!/bin/sh + backend="$(git rev-parse --absolute-git-dir)/tickets-fixture" + exec uv run --quiet --frozen --no-dev --project "$backend" python "$backend/tests/eval_gh.py" "$@" + setup: | + mkdir -p .git/tickets-fixture/tests .github/ISSUE_TEMPLATE .git/fixture-gh + cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/tickets-fixture/ + cp -R "{{case_dir}}/../../../backend/src" .git/tickets-fixture/ + cp "{{case_dir}}/../../../backend/tests/eval_gh.py" .git/tickets-fixture/tests/ + cp "{{case_dir}}/../../../backend/src/darrow_tickets/template_assets/bug-fix-regression.md" .github/ISSUE_TEMPLATE/ + git remote add origin https://github.test/o/r.git + printf 'bug\n' > .git/fixture-gh/labels + : > .git/fixture-gh/list +checks: + - name: one bug issue is created + run: test "$(wc -l < .git/fixture-gh/created-ids | tr -d ' ')" -eq 1 && rg -q '^bug$' .git/fixture-gh/created-labels + - name: incident and expected behavior are grounded + run: >- + rg -q 'TypeError: missing header' .git/fixture-gh/created-body && + rg -qi 'validation error' .git/fixture-gh/created-body + - name: same reproducer and regression coverage remain required + run: rg -q 'bun test csv-import' .git/fixture-gh/created-body && rg -qi 'regression' .git/fixture-gh/created-body + - name: before and after evidence remains required + run: rg -qi 'before|original failure' .git/fixture-gh/created-body && rg -qi 'after|passing result' .git/fixture-gh/created-body diff --git a/plugins/capability/darrow-tickets/skills/create-ticket/evals/custom-template.yaml b/plugins/capability/darrow-tickets/skills/create-ticket/evals/custom-template.yaml new file mode 100644 index 00000000..2ffa428a --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/create-ticket/evals/custom-template.yaml @@ -0,0 +1,55 @@ +id: create-ticket-custom-template +invariant: TM-T3,TM-C11 +activation: competition +mount_plugin_skills: true +prompt: >- + File a feature issue for customer data export. Our support team needs a CSV + of account records for authorized requests. Use the repository's issue + template for this feature. We do not know whether audit logging is required. +fixture: + commits: + - message: "chore: init" + files: + README.md: "# Fixture\n" + files: + .github/ISSUE_TEMPLATE/customer-export.yml: | + name: Customer data export + description: Requests for authorized account export + body: + - type: textarea + attributes: + label: Outcome + description: Identify who needs the export and which records it contains. + - type: textarea + attributes: + label: Acceptance criteria + description: Require an authorization check and evidence that unauthorized callers cannot export. + - type: textarea + attributes: + label: Audit logging + description: Record whether logging is required or remains open. + bin: + gh: | + #!/bin/sh + backend="$(git rev-parse --absolute-git-dir)/tickets-fixture" + exec uv run --quiet --frozen --no-dev --project "$backend" python "$backend/tests/eval_gh.py" "$@" + setup: | + mkdir -p .git/tickets-fixture/tests .git/fixture-gh + cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/tickets-fixture/ + cp -R "{{case_dir}}/../../../backend/src" .git/tickets-fixture/ + cp "{{case_dir}}/../../../backend/tests/eval_gh.py" .git/tickets-fixture/tests/ + git remote add origin https://github.test/o/r.git + printf 'enhancement\n' > .git/fixture-gh/labels + : > .git/fixture-gh/list +checks: + - name: one issue is created + run: test "$(wc -l < .git/fixture-gh/created-ids | tr -d ' ')" -eq 1 + - name: custom authorization requirement survives + run: >- + rg -qi 'authoriz(s|e|ation)' .git/fixture-gh/created-body && + rg -qi 'unauthori[sz]ed' .git/fixture-gh/created-body && + rg -qi 'cannot export|can.t export|den(y|ied)|reject|block|prevent' .git/fixture-gh/created-body + - name: logging uncertainty remains explicit + run: >- + rg -qi 'audit[ -]logging|audit logging' .git/fixture-gh/created-body && + rg -Uqi '(?s)audit[ -]logging.{0,200}(open|unknown|undecided|whether|not decided|to be determined|pending)|## Open questions.{0,500}audit[ -]logging' .git/fixture-gh/created-body diff --git a/plugins/capability/darrow-tickets/skills/create-ticket/evals/dependency-template.yaml b/plugins/capability/darrow-tickets/skills/create-ticket/evals/dependency-template.yaml new file mode 100644 index 00000000..1a7c86bb --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/create-ticket/evals/dependency-template.yaml @@ -0,0 +1,41 @@ +id: create-ticket-dependency-template +invariant: TM-T1,TM-T3,TM-C4,TM-C11 +activation: positive +mount_plugin_skills: true +prompt: >- + File a dependency-upgrade issue for httpx from 0.27.2 to 0.28.1. Our API + client uses it in clients/api.py. We have not reviewed the release notes yet. +fixture: + commits: + - message: "chore: init" + files: + README.md: "# Fixture\n" + clients/api.py: "import httpx\n" + bin: + gh: | + #!/bin/sh + backend="$(git rev-parse --absolute-git-dir)/tickets-fixture" + exec uv run --quiet --frozen --no-dev --project "$backend" python "$backend/tests/eval_gh.py" "$@" + setup: | + mkdir -p .git/tickets-fixture/tests .github/ISSUE_TEMPLATE .git/fixture-gh + cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/tickets-fixture/ + cp -R "{{case_dir}}/../../../backend/src" .git/tickets-fixture/ + cp "{{case_dir}}/../../../backend/tests/eval_gh.py" .git/tickets-fixture/tests/ + cp "{{case_dir}}/../../../backend/src/darrow_tickets/template_assets/dependency-upgrade.md" .github/ISSUE_TEMPLATE/ + printf '\n## Local acceptance criteria\n\n- [ ] Run the clients/api.py retry-path check after the upgrade.\n' >> .github/ISSUE_TEMPLATE/dependency-upgrade.md + git remote add origin https://github.test/o/r.git + printf 'enhancement\n' > .git/fixture-gh/labels + : > .git/fixture-gh/list +checks: + - name: one issue is created + run: test "$(wc -l < .git/fixture-gh/created-ids | tr -d ' ')" -eq 1 + - name: dependency and target are grounded + run: rg -q 'httpx' .git/fixture-gh/created-body && rg -q '0\.28\.1' .git/fixture-gh/created-body + - name: release notes remain required + run: rg -qi 'release notes|changelog' .git/fixture-gh/created-body + - name: migration and affected behavior remain required + run: rg -qi 'breaking changes|migration requirements' .git/fixture-gh/created-body + - name: compatibility evidence remains required + run: rg -qi 'compatibility evidence|affected behavior' .git/fixture-gh/created-body + - name: customized retry-path criterion survives + run: rg -qi 'retry.path' .git/fixture-gh/created-body diff --git a/plugins/capability/darrow-tickets/skills/create-ticket/evals/retirement-template.yaml b/plugins/capability/darrow-tickets/skills/create-ticket/evals/retirement-template.yaml new file mode 100644 index 00000000..f0fbbac6 --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/create-ticket/evals/retirement-template.yaml @@ -0,0 +1,40 @@ +id: create-ticket-retirement-template +invariant: TM-T1,TM-T3,TM-C11 +activation: competition +mount_plugin_skills: true +prompt: >- + Create one ticket to retire the LEGACY_READER flag and its old reader path. + Remove its configuration and old reader tests and documentation. The modern + reader must keep working. We have not decided whether existing callers need + a migration note. +fixture: + commits: + - message: "chore: init" + files: + README.md: "# Fixture\n" + reader.py: "LEGACY_READER = True\n" + bin: + gh: | + #!/bin/sh + backend="$(git rev-parse --absolute-git-dir)/tickets-fixture" + exec uv run --quiet --frozen --no-dev --project "$backend" python "$backend/tests/eval_gh.py" "$@" + setup: | + mkdir -p .git/tickets-fixture/tests .github/ISSUE_TEMPLATE .git/fixture-gh + cp "{{case_dir}}/../../../backend/pyproject.toml" "{{case_dir}}/../../../backend/uv.lock" .git/tickets-fixture/ + cp -R "{{case_dir}}/../../../backend/src" .git/tickets-fixture/ + cp "{{case_dir}}/../../../backend/tests/eval_gh.py" .git/tickets-fixture/tests/ + cp "{{case_dir}}/../../../backend/src/darrow_tickets/template_assets/retirement.md" .github/ISSUE_TEMPLATE/ + git remote add origin https://github.test/o/r.git + printf 'enhancement\n' > .git/fixture-gh/labels + : > .git/fixture-gh/list +checks: + - name: one issue is created + run: test "$(wc -l < .git/fixture-gh/created-ids | tr -d ' ')" -eq 1 + - name: named retirement target is present + run: rg -q 'LEGACY_READER' .git/fixture-gh/created-body + - name: removal scope covers implementation and configuration + run: rg -qi 'implementation|old reader' .git/fixture-gh/created-body && rg -qi 'config' .git/fixture-gh/created-body + - name: retained behavior and completion evidence remain required + run: rg -qi 'modern reader|retained behavior' .git/fixture-gh/created-body && rg -qi 'complete|evidence' .git/fixture-gh/created-body + - name: migration uncertainty is explicit + run: rg -qi 'migration' .git/fixture-gh/created-body && rg -qi 'open questions|undecided|unknown|not decided' .git/fixture-gh/created-body diff --git a/plugins/capability/darrow-tickets/skills/install-ticket-templates/SKILL.md b/plugins/capability/darrow-tickets/skills/install-ticket-templates/SKILL.md new file mode 100644 index 00000000..89445206 --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/install-ticket-templates/SKILL.md @@ -0,0 +1,52 @@ +--- +name: install-ticket-templates +description: Install Darrow's reusable GitHub issue templates for dependency upgrades, retirement, and bug fixes with regression coverage into a named repository. Use when asked to adopt or install these templates. Do not select for creating a ticket, editing an individual issue, or delivering the work in a ticket. +--- + +# Install ticket templates + +Add the bundled GitHub issue templates to one target repository while preserving +its existing templates and customizations. This changes local repository files; +it does not create issues or start delivery. + +## 1. Identify the target + +Use the repository explicitly named by the user, or the current repository when +the request says “here” or otherwise clearly refers to it. Resolve its absolute +root. Ask for the target if more than one repository could be meant. Do not +infer a different repository from an issue URL or another plugin's files. + +**Complete when:** one target repository root and the user's installation +request are established. + +## 2. Install the bundled files + +Run the contained package through its frozen runtime entrypoint. `` +is the absolute directory containing this `SKILL.md`: + +```sh +uv run --quiet --no-project "/../../backend/scripts/run_locked.py" darrow-ticket-templates install --repo +``` + +The command creates only absent `dependency-upgrade.md`, `retirement.md`, and +`bug-fix-regression.md` files under `.github/ISSUE_TEMPLATE/`. It reports each +absolute path as `created`, `unchanged`, or `preserved`. A differing file, +symlink, directory, or unrelated repository template is never overwritten. +An existing case-variant filename that blocks a bundled path causes a refusal. +Treat an error as a refusal and report it; do not copy files manually, delete a +customization, or retry under a different path. + +**Complete when:** the command has reported the state of all three template +paths, or its refusal has been returned without a replacement write. + +## 3. Report the local result + +Report the created, unchanged, and preserved absolute paths returned by the +command, including any customization +that needs human review before the bundled version can be adopted. Do not +commit, push, create a ticket, invoke `ticket-to-pr`, or claim the templates are +available in GitHub's issue chooser until they reach the repository's default +branch through separately authorized work. + +**Complete when:** the user can see exactly which local files changed and which +existing files were left alone. diff --git a/plugins/capability/darrow-tickets/skills/install-ticket-templates/agents/openai.yaml b/plugins/capability/darrow-tickets/skills/install-ticket-templates/agents/openai.yaml new file mode 100644 index 00000000..56e46820 --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/install-ticket-templates/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Install Ticket Templates" + short_description: "Add reusable GitHub issue templates" + default_prompt: "Use $install-ticket-templates to add the Darrow issue templates to this repository." diff --git a/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/install-new.yaml b/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/install-new.yaml new file mode 100644 index 00000000..54827c37 --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/install-new.yaml @@ -0,0 +1,25 @@ +id: install-ticket-templates-new +invariant: TM-T1,TM-T2,TM-T4 +activation: positive +mount_plugin_skills: true +prompt: "{{skill_invocation}} Install Darrow's issue templates in this repository." +fixture: + commits: + - message: "chore: init" + files: + README.md: "# Fixture\n" +checks: + - name: three templates installed + run: >- + test -f .github/ISSUE_TEMPLATE/dependency-upgrade.md && + test -f .github/ISSUE_TEMPLATE/retirement.md && + test -f .github/ISSUE_TEMPLATE/bug-fix-regression.md + - name: upgrade and regression criteria are present + run: >- + rg -q 'compatibility evidence' .github/ISSUE_TEMPLATE/dependency-upgrade.md && + rg -q 'before/after evidence' .github/ISSUE_TEMPLATE/bug-fix-regression.md + - name: installation does not start delivery + run: test "$(git branch --show-current)" = main && test "$(git rev-list --count HEAD)" -eq 1 +output_checks: + - name: local file results are reported + expect_regex: 'dependency-upgrade\.md' diff --git a/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/list-only.yaml b/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/list-only.yaml new file mode 100644 index 00000000..10ebfb23 --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/list-only.yaml @@ -0,0 +1,16 @@ +id: install-ticket-templates-list-only +invariant: TM-T2 +activation: negative +mount_plugin_skills: true +activation_excludes: + - install-ticket-templates +prompt: "Which GitHub issue templates are already present in this repository? Just list them." +fixture: + commits: + - message: "chore: init" + files: + README.md: "# Fixture\n" + .github/ISSUE_TEMPLATE/custom-triage.md: "---\nname: Custom triage\nabout: Triage reports\n---\n" +checks: + - name: no template files are installed + run: test ! -e .github/ISSUE_TEMPLATE/dependency-upgrade.md && test ! -e .github/ISSUE_TEMPLATE/retirement.md && test ! -e .github/ISSUE_TEMPLATE/bug-fix-regression.md diff --git a/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/preserve-existing.yaml b/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/preserve-existing.yaml new file mode 100644 index 00000000..f3da298c --- /dev/null +++ b/plugins/capability/darrow-tickets/skills/install-ticket-templates/evals/preserve-existing.yaml @@ -0,0 +1,25 @@ +id: install-ticket-templates-preserve-existing +invariant: TM-T2 +activation: positive +mount_plugin_skills: true +prompt: "{{skill_invocation}} Add Darrow's issue templates here, keeping our customizations." +fixture: + commits: + - message: "chore: init" + files: + README.md: "# Fixture\n" + .github/ISSUE_TEMPLATE/dependency-upgrade.md: "CUSTOM DEPENDENCY WORKFLOW\n" + .github/ISSUE_TEMPLATE/custom-triage.md: "CUSTOM TRIAGE\n" + .github/ISSUE_TEMPLATE/config.yml: "blank_issues_enabled: false\n" +checks: + - name: customized upgrade template is preserved + run: test "$(cat .github/ISSUE_TEMPLATE/dependency-upgrade.md)" = 'CUSTOM DEPENDENCY WORKFLOW' + - name: unrelated custom files are preserved + run: >- + test "$(cat .github/ISSUE_TEMPLATE/custom-triage.md)" = 'CUSTOM TRIAGE' && + test "$(cat .github/ISSUE_TEMPLATE/config.yml)" = 'blank_issues_enabled: false' + - name: remaining bundled templates are installed + run: test -f .github/ISSUE_TEMPLATE/retirement.md && test -f .github/ISSUE_TEMPLATE/bug-fix-regression.md +output_checks: + - name: preserved customization is reported + expect_regex: '[Pp]reserved.*dependency-upgrade\.md|dependency-upgrade\.md.*[Pp]reserved'