feat(kvb-git): add pr-create, pr-label and issue-solve; rename close-issues to issue-close

kvb-git-pr-create opens a pull request for the current branch: it assesses the branch, hands the push to the operator (gipsn) and waits until git ls-remote shows the head, writes a body with one Closes line per verified issue, confirms once, and opens it ready at the kvb-org-split bar or as a draft below it, then runs kvb-git-pr-label. kvb-git-pr-label sets hotfix on a pull request that targets the default branch and fixes a failure there, and 90/5 once it meets the split bar, each with its evidence, creating the labels once and marking a draft ready when it gains 90/5. kvb-git-issue-solve solves open issues one sidetree each through kvb-org-split, skipping fixed ones and those a peer already works on, and opens one pull request per issue.

kvb-git-close-issues becomes kvb-git-issue-close, following the kvb-git-<object>-<verb> scheme: with a pull request it works as before; without one it covers every open issue, and the new done.py lists the default-branch commits that reference each (one git log, foreign repositories excluded). tests/test_close_issues.py moves to tests/test_issue_close.py and gains TestDone on a temporary repository. Six skill descriptions are shortened, keeping their triggers, so the listing stays within its 1500-word budget (1517 to 1493).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-04 17:07:22 +01:00
1 parent 32f58712e5
commit f815060584
16 files changed
+399 -134

No files matched your search

+15 -3
View File
@@ -49,7 +49,10 @@ flowchart LR
git --> git_commit_next[kvb-git-commit-next]
git --> git_commit_no_verify[kvb-git-commit-no-verify]
git --> git_merge_fix[kvb-git-merge-fix]
git --> git_close_issues[kvb-git-close-issues]
git --> git_issue_close[kvb-git-issue-close]
git --> git_issue_solve[kvb-git-issue-solve]
git --> git_pr_create[kvb-git-pr-create]
git --> git_pr_label[kvb-git-pr-label]
git --> git_update_branches[kvb-git-update-branches]
git --> git_branch_gc[kvb-git-branch-gc]
git --> git_changelog[kvb-git-changelog]
@@ -128,7 +131,10 @@ flowchart LR
commit_next[kvb-git-commit-next]
commit_no_verify[kvb-git-commit-no-verify]
merge_fix[kvb-git-merge-fix]
close_issues[kvb-git-close-issues]
issue_close[kvb-git-issue-close]
issue_solve[kvb-git-issue-solve]
pr_create[kvb-git-pr-create]
pr_label[kvb-git-pr-label]
update_branches[kvb-git-update-branches]
branch_gc[kvb-git-branch-gc]
changelog[kvb-git-changelog]
@@ -186,7 +192,13 @@ flowchart LR
commit_next --> commit_no_verify
commit_next --> explain
commit_next --> dialectic
close_issues --> confidence
issue_close --> confidence
issue_solve --> issue_close
issue_solve --> funnel
issue_solve --> pr_create
pr_create --> issue_close
pr_create --> pr_label
pr_create -. push .-> operator
update_branches --> active_listening
update_branches -. conflicts .-> merge_fix
update_branches -. pushes .-> operator
+3 -3
View File
@@ -1,9 +1,9 @@
---
name: kvb-check-security
description: >
Run the MEDUSA scanner (medusa-security) on a repository, working tree or
secret stores. Use for security scans, vetting a repo, leaked secrets in
chat or shell history, or /kvb-check-security. Defensive only.
Run the MEDUSA scanner on a repository, working tree or secret stores:
security scans, vetting a repo, leaked secrets in chat or shell history, or
/kvb-check-security. Defensive only.
---
MEDUSA is a defensive AI-first security scanner (40,000+ patterns: CVEs, prompt injection, MCP tool poisoning, leaked secrets).
+3 -3
View File
@@ -1,9 +1,9 @@
---
name: kvb-cleanup
description: >
Free disk space agents filled: ask peers, inventory /tmp, Docker and agent
dirs, and delete only what the operator picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen", "platte voll", "no
space left on device".
Free disk space agents filled in /tmp, Docker and agent dirs; delete only
what the operator picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen",
"platte voll", "no space left on device".
---
Nothing is deleted before the operator has picked it. Every candidate carries
+3 -3
View File
@@ -1,9 +1,9 @@
---
name: kvb-code-scope-guard
description: >
Deliver exactly what was asked and nothing more. Trigger on every task that
creates files, commands or commits, on autonomous runs and hand-overs, and
when asked why something unrequested exists.
Deliver exactly what was asked. Trigger on every task that creates files,
commands or commits, on autonomous runs and hand-overs, and when asked why
something unrequested exists.
---
The requested scope is the deliverable. A problem you noticed behind the
+3 -3
View File
@@ -1,9 +1,9 @@
---
name: kvb-create-plan
description: >
Turn a request into an objective-oriented plan and hand it to the kvb-org-robot
skill. Trigger on /kvb-create-plan or when the operator asks for a plan, breakdown or
strategy before work starts.
Turn a request into an objective-oriented plan for the kvb-org-robot skill.
Trigger on /kvb-create-plan or a request for a plan, breakdown or strategy
before work starts.
---
Produce a plan whose every item is an outcome with a check, then execute it
+3 -4
View File
@@ -1,10 +1,9 @@
---
name: kvb-git-changelog
description: >
Write the release message for `pkgmgr release`;
prints only, never edits, tags or releases. Trigger on
/kvb-git-changelog, "release message", "changelog entry", before cutting a release,
or on pkgmgr's markdown-lint rejection.
Write the release message for `pkgmgr release`; prints only. Trigger on
/kvb-git-changelog, "release message", "changelog entry", before cutting a
release, or on pkgmgr's markdown-lint rejection.
---
The message is one text that pkgmgr writes to four places: the CHANGELOG.md
-50
View File
@@ -1,50 +0,0 @@
---
name: kvb-git-close-issues
description: >
Close the issues a pull request fixes, each verified and confirmed.
Trigger on /kvb-git-close-issues [pr].
---
Closing an issue is outward-facing: nothing is closed or edited before the
operator confirms the list, and every closed issue names the pull request
and commit that fixed it.
## Step 1: candidates
The pull request named after the command, else the one of the current
branch (`gh pr view --json number`). `${CLAUDE_SKILL_DIR}/scripts/issues.py`
lists every issue it references, from closing keywords and mentions in the
body and the commits, a trailing number in the branch name, and the issues
GitHub already links:
```bash
gh pr view <pr> --json number,body,headRefName,commits,closingIssuesReferences | issues.py --repo <owner/name>
```
## Step 2: verify
Per candidate, `gh issue view <n> --json state,title,body,comments` (with
`--repo` for a foreign one); a closed issue is skipped. Judge it `fixed`,
`partly` or `unrelated` against the pull request's diff, with evidence: the
change that addresses each point of the issue and the test or CI run that
proves it. Only `fixed` goes on; `partly` gets a progress comment instead.
## Step 3: confirm
`AskUserQuestion` with `multiSelect: true`, one option per `fixed` issue,
the evidence in its description, nothing preselected.
## Step 4: close
Where the fix lives decides how:
- **Merged into the default branch**: `gh issue close <n> --reason completed
--comment "Fixed by #<pr> (<sha>)."`.
- **Open, or merged into another branch**: add one `Closes #<n>` line per
issue to the pull request body (`gh pr edit <pr> --body-file <file>`,
keeping the existing body), so the merge into the default branch closes
them; never close them early.
## Output
| Issue | Verdict | Evidence | Action |
+61
View File
@@ -0,0 +1,61 @@
---
name: kvb-git-issue-close
description: >
Close finished issues, of one pull request or the whole repository, each
verified and confirmed. Trigger on /kvb-git-issue-close [pr].
---
Closing an issue is outward-facing: nothing is closed or edited before the
operator confirms the list, and every closed issue names the pull request or
commit that fixed it.
## Step 1: candidates
- **A pull request named** (or the current branch's, `gh pr view --json
number`): `${CLAUDE_SKILL_DIR}/scripts/issues.py` lists every issue it
references, from closing keywords and mentions in the body and the commits,
a trailing number in the branch name, and the issues GitHub already links:
```bash
gh pr view <pr> --json number,body,headRefName,commits,closingIssuesReferences | issues.py --repo <owner/name>
```
- **No pull request**: every open issue of the repository. After `git fetch`,
`${CLAUDE_SKILL_DIR}/scripts/done.py` lists the default-branch commits that
reference each one:
```bash
gh issue list --state open --limit 1000 --json number | done.py --repo <owner/name> --git-dir . --ref origin/<default>
```
An open issue no commit references can still be fixed: search the default
branch by its description with the kvb-git-merge-fix skill's `find_fix.py`.
## Step 2: verify
Per candidate, `gh issue view <n> --json state,title,body,comments` (with
`--repo` for a foreign one); a closed issue is skipped. Judge it `fixed`,
`partly` or `unrelated` against the diff, with evidence: the change that
addresses each point of the issue and the test or CI run that proves it.
Only `fixed` goes on; `partly` gets a progress comment instead.
## Step 3: confirm
`AskUserQuestion` with `multiSelect: true`, one option per `fixed` issue,
the evidence in its description, nothing preselected.
## Step 4: close
Where the fix lives decides how:
- **On the default branch**: `gh issue close <n> --reason completed
--comment "Fixed by #<pr> (<sha>)."`, or the commit alone when no pull
request carried it.
- **In an open pull request, or merged into another branch**: add one
`Closes #<n>` line per issue to the pull request body (`gh pr edit <pr>
--body-file <file>`, keeping the existing body), so the merge into the
default branch closes them; never close them early.
## Output
| Issue | Verdict | Evidence | Action |
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""List the default-branch commits that reference each open issue.
Args:
stdin: JSON list of open issues, as `gh issue list --state open --json
number` prints it.
--repo: owner/name of the repository; its own references count, foreign
ones do not.
--git-dir: path of a local clone, fetched.
--ref: the default branch to scan, e.g. origin/main.
--json: print the result as JSON instead of tab-separated lines.
Output:
one line per open issue that a commit references: issue, closing or
mention, the short shas.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from issues import PATTERN, fail
def references(log: str, own: str, open_issues: set[int]) -> dict[int, dict]:
found: dict[int, dict] = {}
for record in filter(None, log.split("\x1e")):
sha, _, message = record.strip("\n").partition("\x1f")
for match in PATTERN.finditer(message):
repo = match.group("repo") or match.group("url") or ""
number = int(match.group("num"))
if number not in open_issues or (repo and repo.lower() != own.lower()):
continue
entry = found.setdefault(number, {"closing": False, "commits": []})
entry["closing"] = entry["closing"] or bool(match.group("kw"))
if sha[:10] not in entry["commits"]:
entry["commits"].append(sha[:10])
return found
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo", required=True)
parser.add_argument("--git-dir", required=True)
parser.add_argument("--ref", required=True)
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
try:
issues = json.load(sys.stdin)
open_issues = {int(issue["number"]) for issue in issues}
except (ValueError, TypeError, KeyError):
fail("stdin is not the JSON of gh issue list --json number")
log = subprocess.run(
["git", "log", "--format=%H%x1f%B%x1e", args.ref],
cwd=args.git_dir,
capture_output=True,
text=True,
check=False,
)
if log.returncode != 0:
fail(log.stderr.strip() or f"git log {args.ref} failed")
found = references(log.stdout, args.repo, open_issues)
rows = [{"issue": number, **found[number]} for number in sorted(found)]
if args.json:
print(json.dumps(rows, indent=1))
return 0
for row in rows:
kind = "closing" if row["closing"] else "mention"
print(f"{row['issue']}\t{kind}\t{', '.join(row['commits'])}")
return 0
if __name__ == "__main__":
sys.exit(main())
+35
View File
@@ -0,0 +1,35 @@
---
name: kvb-git-issue-solve
description: >
Solve open issues, each in its own worktree, and open one pull request per
issue. Trigger on /kvb-git-issue-solve [issue...].
---
Every issue gets its own sidetree, its own evidence and its own pull
request, so a red one blocks nothing else.
## Procedure
1. **Candidates**: the issues named, else `gh issue list --state open`. Drop
what the `kvb-git-issue-close` skill finds already fixed, and what a peer
already works on: `council.py list` and the `kvb-org-organize` Peer
protocol; when a peer solves it, only comment on the issue who solves it
where.
2. **Rank**: issues about a failing default branch first, then the rank of
the `kvb-org-wsjf` skill; the operator's order wins.
3. **Solve** each with the `kvb-org-split` skill: claim `work/issue-<n>`,
enter it in the `kvb-org-rasci` register (objective: the issue fixed; key
results: a test that failed before and passes after, the pull request
open), branch `fix/<scope>/<n>` from the default branch in its own
worktree, evidence, self-assessment, independent review. Commits go to
that branch only, never to the default branch; parallel work follows the
resource chair's grants.
4. **Open the pull request** with the `kvb-git-pr-create` skill, closing
exactly its issue. Below the split bar the proposal becomes a draft pull
request with the residuals and the evidence that would close them.
5. **Report** per issue, then release its claim and tear its sidetree down
as `kvb-org-split` describes; the branch stays until the merge.
## Output
| Issue | Branch | Confidence | Break risk | PR | State |
+35
View File
@@ -0,0 +1,35 @@
---
name: kvb-git-pr-create
description: >
Open a pull request for the current branch: push handed over, Closes lines,
labels. Trigger on /kvb-git-pr-create.
---
Opening a pull request is outward-facing and needs the branch on the remote,
which only the operator pushes. Nothing is created before the operator
confirms it.
## Procedure
1. **Check**: the branch is committed and clean, branched from the default
branch (`gh repo view --json defaultBranchRef`), and has no open pull
request yet (`gh pr list --head <branch>`).
2. **Assess** the branch with the `kvb-check-confidence` skill: fix
confidence, break risk, residuals.
3. **Push**: hand the operator `gipsn` in the worktree through the
`kvb-org-operator` skill, and continue once `git ls-remote --heads origin
<branch>` shows the branch's head commit.
4. **Body**, written to a scratch file: what changed and why, the test
evidence, the confidence block, and one `Closes #<n>` line per issue the
branch fixes, each verified as the `kvb-git-issue-close` skill does.
5. **Confirm** with one `AskUserQuestion`: title (Conventional Commits),
base, state and labels. The state follows the bar of the `kvb-org-split`
skill: ready at or above it, a draft with the residuals in the body below
it.
6. **Create**: `gh pr create --base <default> --head <branch> --title
"<title>" --body-file <file>`, plus `--draft` below the bar.
7. **Label** with the `kvb-git-pr-label` skill.
## Output
| PR | Base | State | Labels | Closes |
+34
View File
@@ -0,0 +1,34 @@
---
name: kvb-git-pr-label
description: >
Label pull requests `hotfix` and `90/5` from evidence. Trigger on
/kvb-git-pr-label [pr]; kvb-git-pr-create runs it.
---
Two labels, each set only with its evidence and removed when it no longer
holds:
- **`hotfix`**: the pull request targets the default branch and fixes a
failure on it, a red default-branch run or an issue that reproduces there;
name the run or issue.
- **`90/5`**: the pull request meets the bar of the `kvb-org-split` skill (fix
confidence of at least 90 %, break risk under 5 %), from the
`kvb-check-confidence` numbers in its body or freshly assessed.
## Procedure
1. Target: the pull request named, else the current branch's (`gh pr view
--json number,baseRefName,headRefName,isDraft,labels,body`); the default
branch from `gh repo view --json defaultBranchRef`.
2. Judge both labels with their evidence.
3. A missing label is created once: `gh label create hotfix --color B60205
--description "Fixes a failure on the default branch"`, `gh label create
90/5 --color 0E8A16 --description "Fix confidence >= 90 %, break risk < 5 %"`.
4. Standalone, confirm the changes with one `AskUserQuestion`; from
`kvb-git-pr-create`, its confirmation covers them.
5. `gh pr edit <pr> --add-label <l>` or `--remove-label <l>`. A draft that
gains `90/5` also gets `gh pr ready <pr>`.
## Output
| PR | Label | Change | Evidence |
+2 -2
View File
@@ -2,8 +2,8 @@
name: kvb-org-split
description: >
Keep agent work in its own worktree until 90% fix confidence and under 5%
break risk; below that it becomes a proposal. Trigger when delegating to or
parallelizing agents, or on /kvb-org-split.
break risk, else a proposal. Trigger when delegating or parallelizing, or on
/kvb-org-split.
---
# Funnel
-63
View File
@@ -1,63 +0,0 @@
"""Exercise issues.py: closing keywords, mentions, commits, branch number and linked issues."""
from __future__ import annotations
import json
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "skills" / "kvb-git-close-issues" / "scripts" / "issues.py"
PR = {
"number": 40,
"body": (
"Fixes #12 and touches #13.\nCloses: other/repo#7\nSee https://github.com/a/b/issues/9\n"
"Rows WordPress#0 and docs#1 are not issues; this is #40."
),
"headRefName": "fix/sys-ctl-hlth-runtime/vulnerability-engines/1324",
"commits": [
{"oid": "abcdef1234567", "messageHeadline": "fix: guard", "messageBody": "Resolved #13"},
{"oid": "1111111111111", "messageHeadline": "chore: tidy", "messageBody": ""},
],
"closingIssuesReferences": [
{"number": 21, "url": "https://github.com/owner/repo/issues/21"}
],
}
def _run(payload: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPT), "--repo", "Owner/repo", "--json"],
input=payload,
check=False,
capture_output=True,
text=True,
)
class TestIssues(unittest.TestCase):
def test_every_reference_is_found_with_its_kind_and_source(self):
result = _run(json.dumps(PR))
self.assertEqual(result.returncode, 0, result.stderr)
rows = {(r["repo"], r["issue"]): r for r in json.loads(result.stdout)}
self.assertEqual(
{key: (row["closing"], row["sources"]) for key, row in rows.items()},
{
("", 12): (True, ["body"]),
("", 13): (True, ["body", "commit abcdef1234"]),
("other/repo", 7): (True, ["body"]),
("a/b", 9): (False, ["body"]),
("", 1324): (False, ["branch"]),
("", 21): (True, ["linked"]),
},
)
def test_invalid_input_exits_2(self):
self.assertEqual(_run("not json").returncode, 2)
self.assertEqual(_run("[]").returncode, 2)
if __name__ == "__main__":
unittest.main()
+122
View File
@@ -0,0 +1,122 @@
"""Exercise issues.py (one pull request's issues) and done.py (open issues the default branch references)."""
from __future__ import annotations
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "skills" / "kvb-git-issue-close" / "scripts" / "issues.py"
DONE = SCRIPT.with_name("done.py")
PR = {
"number": 40,
"body": (
"Fixes #12 and touches #13.\nCloses: other/repo#7\nSee https://github.com/a/b/issues/9\n"
"Rows WordPress#0 and docs#1 are not issues; this is #40."
),
"headRefName": "fix/sys-ctl-hlth-runtime/vulnerability-engines/1324",
"commits": [
{"oid": "abcdef1234567", "messageHeadline": "fix: guard", "messageBody": "Resolved #13"},
{"oid": "1111111111111", "messageHeadline": "chore: tidy", "messageBody": ""},
],
"closingIssuesReferences": [
{"number": 21, "url": "https://github.com/owner/repo/issues/21"}
],
}
def _run(payload: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPT), "--repo", "Owner/repo", "--json"],
input=payload,
check=False,
capture_output=True,
text=True,
)
class TestIssues(unittest.TestCase):
def test_every_reference_is_found_with_its_kind_and_source(self):
result = _run(json.dumps(PR))
self.assertEqual(result.returncode, 0, result.stderr)
rows = {(r["repo"], r["issue"]): r for r in json.loads(result.stdout)}
self.assertEqual(
{key: (row["closing"], row["sources"]) for key, row in rows.items()},
{
("", 12): (True, ["body"]),
("", 13): (True, ["body", "commit abcdef1234"]),
("other/repo", 7): (True, ["body"]),
("a/b", 9): (False, ["body"]),
("", 1324): (False, ["branch"]),
("", 21): (True, ["linked"]),
},
)
def test_invalid_input_exits_2(self):
self.assertEqual(_run("not json").returncode, 2)
self.assertEqual(_run("[]").returncode, 2)
class TestDone(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.repo = Path(tmp.name)
self.env = {
"PATH": "/usr/bin:/bin",
"HOME": tmp.name,
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@example.org",
"GIT_COMMITTER_NAME": "t",
"GIT_COMMITTER_EMAIL": "t@example.org",
}
self._git("init", "-q", "-b", "main")
for message in (
"fix: guard\n\nFixes #3",
"docs: mention #4 and other/repo#6",
"fix: port\n\nCloses Owner/repo#5",
"chore: tidy #3",
):
self._git("commit", "-q", "--allow-empty", "-m", message)
self.shas = self._git("log", "--format=%H", "main").split()
def _git(self, *args: str) -> str:
return subprocess.run(
["git", *args], cwd=self.repo, env=self.env, capture_output=True, text=True, check=True
).stdout
def _run(self, payload: str, ref: str = "main") -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(DONE), "--repo", "owner/Repo", "--git-dir", str(self.repo), "--ref", ref, "--json"],
input=payload,
capture_output=True,
text=True,
check=False,
)
def test_open_issues_with_their_commits(self):
result = self._run(json.dumps([{"number": n} for n in (3, 4, 5, 6, 7)]))
self.assertEqual(result.returncode, 0, result.stderr)
newest, closes5, mentions4, fixes3 = (sha[:10] for sha in self.shas)
self.assertEqual(
json.loads(result.stdout),
[
{"issue": 3, "closing": True, "commits": [newest, fixes3]},
{"issue": 4, "closing": False, "commits": [mentions4]},
{"issue": 5, "closing": True, "commits": [closes5]},
],
)
def test_bad_input_or_ref_exits_2(self):
self.assertEqual(self._run("not json").returncode, 2)
self.assertEqual(self._run('[{"id": 1}]').returncode, 2)
self.assertEqual(self._run("[]", ref="nope").returncode, 2)
if __name__ == "__main__":
unittest.main()