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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "doperpowers",
"description": "Fork of Superpowers (obra/superpowers): a full software-development methodology for coding agents — brainstorm, plan, subagent-driven TDD, review — plus fork-specific skills like orchestrating-daemons.",
"version": "7.3.0",
"version": "7.4.0",
"source": "./",
"author": {
"name": "SSFSKIM",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "doperpowers",
"description": "Fork of Superpowers (obra/superpowers): a full software-development methodology for coding agents — brainstorm, plan, subagent-driven TDD, review — plus fork-specific skills like orchestrating-daemons. Derived from Jesse Vincent's MIT-licensed Superpowers.",
"version": "7.3.0",
"version": "7.4.0",
"author": {
"name": "SSFSKIM",
"email": "supremekim17@gmail.com"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doperpowers",
"version": "7.3.0",
"version": "7.4.0",
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
"author": {
"name": "Jesse Vincent",
Expand Down
27 changes: 27 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Doperpowers Release Notes

## v7.4.0 (2026-07-07)

### Issue Tracker — the board's node detail now shows GitHub-linked PRs

Clicking a ticket on the hosted board showed its issue number, created, and
updated dates, but never the pull requests linked to it. The renderer *could*
show a PR — but only from a `pr:` line manually written into the issue body when
a ticket was moved to `in-review`. Tickets that reached **done** the normal way,
via a merged `Closes #N` pull request, never got that line written, so their PR
was invisible. And a single `pr:` value could never represent more than one PR.

This release reads PR links straight from GitHub, where they already exist:

- **`_board.py` snapshot** now fetches `closedByPullRequestsReferences` (the PRs
that close the issue — this is what fills the merge-autoclose gap) and
`CROSS_REFERENCED_EVENT` timeline items (PRs that merely mention the issue).
They are deduped by number into a new per-ticket `prs` list; a PR that both
closes and references keeps the stronger "closes" relation.
- **`board-map.template.html` detail panel** renders that list as a **PRs**
section — one line per PR, linkified, with a state chip (merged / open /
closed / draft) coloured to GitHub's own palette and a "· closes" marker on
closing PRs. Multiple linked PRs are handled natively. The old single `pr:`
meta stays as a fallback when GitHub reports no linkage.

No new dependencies and no change to how the board is rendered or deployed — the
same `board-map.sh --write` on both the GitHub Pages and Cloudflare templates.

## v7.3.0 (2026-07-07)

### Issue Tracker — a private hosted board via Cloudflare Pages + Access
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doperpowers",
"version": "7.3.0",
"version": "7.4.0",
"description": "Doperpowers skills and runtime bootstrap for coding agents",
"type": "module",
"main": ".opencode/plugins/doperpowers.js",
Expand Down
28 changes: 28 additions & 0 deletions skills/issue-tracker/scripts/_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ def _nums(val):
assignees(first:10) { nodes { login } }
parent { number }
blockedBy(first:50) { nodes { number } }
closedByPullRequestsReferences(first:20, includeClosedPrs:true) {
nodes { number url state isDraft }
}
timelineItems(itemTypes:[CROSS_REFERENCED_EVENT], first:40) {
nodes {
... on CrossReferencedEvent {
source { __typename ... on PullRequest { number url state isDraft } }
}
}
}
}
}
}
Expand Down Expand Up @@ -181,6 +191,23 @@ def snapshot(refresh=False):
status = [l[len(STATUS_PREFIX):] for l in labels if l.startswith(STATUS_PREFIX)]
meta = parse_meta(it["body"])
spawned = _nums(meta.get("spawned-by"))
# Native GitHub PR linkage — closesByPR fills the merge-autoclose gap
# (a "Closes #N" merge never writes a pr: meta), cross-refs catch PRs
# that merely mention the issue. Keyed by number → deduped; a PR that
# both closes and cross-refs keeps the stronger "closes" relation.
prs = {}
for pr in it["closedByPullRequestsReferences"]["nodes"]:
if pr:
prs[pr["number"]] = {"num": pr["number"], "url": pr["url"],
"state": pr["state"],
"draft": pr.get("isDraft", False), "rel": "closes"}
for tl in it["timelineItems"]["nodes"]:
src = (tl or {}).get("source") or {}
if src.get("__typename") == "PullRequest":
prs.setdefault(src["number"], {"num": src["number"], "url": src["url"],
"state": src["state"],
"draft": src.get("isDraft", False), "rel": "ref"})
pr_list = sorted(prs.values(), key=lambda p: p["num"])
tickets[str(it["number"])] = {
"id": it["id"],
"title": it["title"],
Expand All @@ -194,6 +221,7 @@ def snapshot(refresh=False):
"relates_to": _nums(meta.get("relates-to")),
"branch": meta.get("branch"),
"pr": meta.get("pr"),
"prs": pr_list,
"labels": [l for l in labels
if not l.startswith(STATUS_PREFIX) and l not in ("bug", "enhancement")],
"assignees": [a["login"] for a in it["assignees"]["nodes"]],
Expand Down
9 changes: 7 additions & 2 deletions skills/issue-tracker/scripts/board-map.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ md = ["# Issue Board", "",
for tid in order:
n = tickets[tid]
title = " ".join(str(n["title"]).split()).replace("|", "\\|")
md.append("| #%s | %s | %s | %s |" % (tid, state_label(tid, n), title, n.get("pr") or ""))
# Prefer the native GitHub-linked PRs (same source the HTML panel uses); fall
# back to the manual pr: meta so a hand-entered PR URL still shows.
prs = n.get("prs") or []
pr_cell = " ".join("#%s" % p["num"] for p in prs) or (n.get("pr") or "")
md.append("| #%s | %s | %s | %s |" % (tid, state_label(tid, n), title, pr_cell))
table = "\n".join(md)
print(table)

Expand Down Expand Up @@ -364,7 +368,8 @@ for t in order:
"blocked_by": [did(b) for b in n.get("blocked_by", [])],
"spawned_by": did(n["spawned_by"]) if n.get("spawned_by") else None,
"relates_to": [did(r) for r in n.get("relates_to", []) or []],
"branch": n.get("branch"), "pr": n.get("pr"), "md": n.get("url"),
"branch": n.get("branch"), "pr": n.get("pr"), "prs": n.get("prs") or [],
"md": n.get("url"),
"created": n.get("created"), "updated": n.get("updated"),
"x": x, "y": y,
})
Expand Down
26 changes: 25 additions & 1 deletion skills/issue-tracker/scripts/board-map.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@
border-top: 1px solid #141a24; font-size: 12px; }
.drow span { color: #556170; } .drow b { font-weight: 500; word-break: break-word; color: #aeb9c6; }
.drow a { color: #93c5fd; text-decoration: none; } .drow a:hover { text-decoration: underline; }
.prlist { display: flex; flex-direction: column; gap: 3px; }
.prline { display: flex; align-items: center; gap: .4rem; }
.prchip { font-size: 9px; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
padding: .02rem .3rem; border-radius: .25rem; border: 1px solid currentColor; opacity: .9; }
.prchip.merged { color: #a371f7; } .prchip.open { color: #3fb950; }
.prchip.closed { color: #f85149; } .prchip.draft { color: #7d8896; }
/* ---- kanban view: state columns sharing the card palette ---- */
#kb { position: absolute; inset: 0; overflow: auto; padding: .8rem;
display: flex; gap: .7rem; align-items: flex-start; }
Expand Down Expand Up @@ -432,6 +438,23 @@
var a = el("a", url); a.href = url; a.target = "_blank"; a.rel = "noopener";
return a;
}
// Native GitHub-linked PRs (closes + cross-ref), one line each with a state chip.
function prList(prs) {
var wrap = el("div", null, "prlist");
prs.forEach(function (p) {
var line = el("div", null, "prline");
var a = link(p.url);
if (a) { a.textContent = "#" + p.num; line.appendChild(a); }
else line.appendChild(el("span", "#" + p.num));
var st = p.state === "MERGED" ? "merged"
: p.state === "CLOSED" ? "closed"
: p.draft ? "draft" : "open";
var chip = el("span", st + (p.rel === "closes" ? " · closes" : ""), "prchip " + st);
line.appendChild(chip);
wrap.appendChild(line);
});
return wrap;
}
function showDetail(n) {
var d = document.getElementById("detail");
d.textContent = "";
Expand All @@ -454,7 +477,8 @@
add("spawned by", n.spawned_by);
add("relates", (n.relates_to || []).join(", "));
add("branch", n.branch);
if (n.pr) add("PR", link(n.pr) || n.pr);
if (n.prs && n.prs.length) rows.appendChild(detailRow("PRs", prList(n.prs)));
else if (n.pr) add("PR", link(n.pr) || n.pr);
if (n.md) add("issue", link(n.md) || n.md);
add("created", n.created);
add("updated", n.updated);
Expand Down
10 changes: 10 additions & 0 deletions tests/issue-tracker/mock-gh/gh
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def main():
"title": opt(argv, "--title") or "", "body": body,
"state": "OPEN", "stateReason": None, "labels": labels,
"assignees": [], "parent": None, "blockedBy": [],
"closesPRs": [], "xrefPRs": [],
"comments": [], "createdAt": "2026-07-06T00:00:00Z",
"updatedAt": "2026-07-06T00:00:00Z",
"url": "https://github.com/%s/issues/%s" % (os.environ.get("BOARD_REPO", "test/repo"), num),
Expand Down Expand Up @@ -173,6 +174,15 @@ def main():
"assignees": {"nodes": [{"login": a} for a in it["assignees"]]},
"parent": {"number": it["parent"]} if it.get("parent") else None,
"blockedBy": {"nodes": [{"number": b} for b in it["blockedBy"]]},
"closedByPullRequestsReferences": {"nodes": [
{"number": p["number"], "url": p["url"],
"state": p.get("state", "MERGED"), "isDraft": p.get("isDraft", False)}
for p in it.get("closesPRs", [])]},
"timelineItems": {"nodes": [
{"source": {"__typename": "PullRequest", "number": p["number"],
"url": p["url"], "state": p.get("state", "OPEN"),
"isDraft": p.get("isDraft", False)}}
for p in it.get("xrefPRs", [])]},
})
print(json.dumps({"data": {"repository": {"issues": {
"pageInfo": {"hasNextPage": False, "endCursor": None},
Expand Down
11 changes: 11 additions & 0 deletions tests/issue-tracker/test-board-scripts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,17 @@ assert_file_exists "$WORK/doperpowers/issue-tracker/BOARD.md" "BOARD.md rendered
assert_equals "$(cat "$WORK/doperpowers/issue-tracker/.gitignore")" "*" "render dir is gitignored"
assert_contains "$(cat "$WORK/doperpowers/issue-tracker/BOARD.html")" '"id": "#9"' "html payload uses display ids"

# native GitHub-linked PRs (closes + cross-ref) surface without any pr: meta —
# the merge-autoclose gap the manual meta could not cover.
python3 -c "import json;p='$MOCK_GH_STATE';s=json.load(open(p));i=s['issues']['9'];i['closesPRs']=[{'number':58,'url':'https://github.com/test/repo/pull/58','state':'MERGED'}];i['xrefPRs']=[{'number':61,'url':'https://github.com/test/repo/pull/61','state':'OPEN'}];json.dump(s,open(p,'w'))"
out="$(run board-map.sh)"
assert_contains "$out" "#58 #61" "md table shows native linked PRs (closes + xref)"
run board-map.sh --write >/dev/null 2>&1
assert_contains "$(cat "$WORK/doperpowers/issue-tracker/BOARD.html")" '"num": 58' "html payload carries closing PR number"
assert_contains "$(cat "$WORK/doperpowers/issue-tracker/BOARD.html")" '"rel": "closes"' "closing PR keeps the closes relation"
assert_contains "$(cat "$WORK/doperpowers/issue-tracker/BOARD.html")" '"num": 61' "html payload carries cross-ref PR number"
assert_contains "$(cat "$WORK/doperpowers/issue-tracker/BOARD.html")" '"rel": "ref"' "cross-ref PR keeps the ref relation"

# ---- worktree friendliness (the v6 guard is gone) --------------------------------
echo "worktree:"
out="$(cd "$TEST_ROOT/wt" && "$SCRIPTS_DIR/board-list.sh")"
Expand Down