diff --git a/.cursor/skills/_lib/pr-skills/README.md b/.cursor/skills/_lib/pr-skills/README.md index c4e92c1739..e759710998 100644 --- a/.cursor/skills/_lib/pr-skills/README.md +++ b/.cursor/skills/_lib/pr-skills/README.md @@ -27,7 +27,7 @@ This directory does not contain a `SKILL.md`; it is not a Cursor skill itself. T | Mode | Pod scope | What it shows | Used by | |---|---|---|---| -| `team` | required (`--pod`) | All open PRs touching the pod's `ownedPaths` that still need reviews. Three sections: needs-your-re-review, stale (>3d), needs-review. PRs with `mergeable: CONFLICTING` are flagged with `⚠️ MERGE CONFLICTS!`. Pass `--authors pod` to additionally scope the dashboard to PRs authored by pod-roster members; non-roster authors touching pod paths are surfaced in a separate "Excluded" section. | `-pr-status` | +| `team` | required (`--pod`) | All open PRs touching the pod's `ownedPaths` (in the primary repo) plus **every open PR** in the pod's `extraRepos` (pod is treated as sole owner there) that still need reviews. Three sections: needs-your-re-review, stale (>3d), needs-review. PRs with `mergeable: CONFLICTING` are flagged with `⚠️ MERGE CONFLICTS!`. Pass `--authors pod` to additionally scope the dashboard to PRs authored by pod-roster members; non-roster authors are surfaced in a separate "Excluded" section. When `extraRepos` resolve, the first output line is a `Repos:` summary and extra-repo PRs render as `owner/repo#`. | `-pr-status` | | `review` | required (`--pod`) | The current user's personal review queue: PRs needing their first review, plus PRs where their review was dismissed. | (currently unused; available for a future skill) | | `my` | optional (`--pod`); cross-pod by default | The current user's own open PRs grouped by merge readiness. Per-PR pod resolution drives ping logic. Emits copy-paste Slack ping messages for missing reviewers. | `qv-pr-mine` | @@ -54,7 +54,7 @@ node .cursor/skills/_lib/pr-skills/pr-status.mjs --pod --mode team --json `--authors` accepts: - `any` (default) — original behavior, all PRs touching pod paths are listed regardless of author. -- `pod` — only PRs authored by `leads ∪ members` from the pod's team JSON. PRs touching pod paths but authored outside the roster are surfaced as a separate "Excluded" section so the pod can still see them for context. Only honored with `--mode team`; ignored (with a warning on stderr) for other modes. +- `pod` — only PRs authored by `leads ∪ members` from the pod's team JSON. PRs touching pod paths (or, for `extraRepos`, any open PR) but authored outside the roster are surfaced as a separate "Excluded" section so the pod can still see them for context. The Excluded section is capped at 10 rendered PRs per repo (a `… +N more in ` line summarizes the rest) so a busy `extraRepos` entry can't bury the dashboard; `--json` always carries the complete list. Only honored with `--mode team`; ignored (with a warning on stderr) for other modes. `pr-status.mjs` reads `~/.config/qvac-pr-skills/config.json` when present for GitHub repo and stale-day settings. If config is missing, the repo is inferred @@ -69,11 +69,14 @@ from the local `upstream` remote. "name": "", "leads": ["", "..."], "members": ["", "..."], - "ownedPaths": ["packages//", "packages//"] + "ownedPaths": ["packages//", "packages//"], + "extraRepos": ["owner/other-repo", "owner/prefix-*"] } ``` - `ownedPaths` are prefix-matched against changed-file paths to decide whether a PR is "owned" by this pod. Use trailing slashes. + `ownedPaths` are prefix-matched against changed-file paths to decide whether a PR is "owned" by this pod in the primary repo. Use trailing slashes. + + `extraRepos` (optional, `--mode team` only) is a list of additional `owner/name` repos the pod owns wholesale: **every** open PR there is in-scope regardless of touched paths. Plain `owner/name` entries are used as-is; an entry whose name segment contains `*` (e.g. `owner/prefix-*`) is treated as a glob and resolved per run against the org's non-archived repos via `gh repo list`. Repos the caller cannot read are skipped with a one-line stderr warning. The primary repo (`config.github.repo`) stays path-filtered — do not duplicate it under `extraRepos`. PRs from extra repos carry a `prRef` of `owner/repo#` (primary-repo PRs keep `#`). 2. Create the per-pod dashboard skill by copying `.cursor/skills/qv-sdk-pr-status/` to `.cursor/skills/qv--pr-status/`. Inside the copy, update the SKILL.md frontmatter (`name:`, `description:`) and the script invocation in the `## Usage` block to swap `--pod sdk` for `--pod `. No other changes required. diff --git a/.cursor/skills/_lib/pr-skills/pr-activity.mjs b/.cursor/skills/_lib/pr-skills/pr-activity.mjs index 6db2aabcbc..b1663f04f9 100644 --- a/.cursor/skills/_lib/pr-skills/pr-activity.mjs +++ b/.cursor/skills/_lib/pr-skills/pr-activity.mjs @@ -11,13 +11,25 @@ export const STATE_ICONS = { }; function gh(args) { + // stderr is piped (not inherited) so it does not leak to the user's terminal + // on success, but is captured on the thrown error so callers can surface a + // meaningful reason (e.g. "Could not resolve to a Repository"). return execFileSync("gh", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, - stdio: ["ignore", "pipe", "ignore"], + stdio: ["ignore", "pipe", "pipe"], }).trim(); } +function ghErrorReason(error) { + const stderr = error?.stderr ? error.stderr.toString().trim() : ""; + const firstLine = (stderr || error?.message || "unknown error") + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0); + return firstLine || "unknown error"; +} + function ghGraphQL(query, jq, vars = {}) { const args = ["api", "graphql", "--raw-field", `query=${query}`]; for (const [k, v] of Object.entries(vars)) { @@ -179,6 +191,75 @@ export function fetchOpenPRs(repoConfig) { return { allPRs, pageNum }; } +const ORG_REPO_LIST_LIMIT = 1000; + +function listOrgRepos(owner) { + const raw = gh([ + "repo", + "list", + owner, + "--no-archived", + "--limit", + String(ORG_REPO_LIST_LIMIT), + "--json", + "name", + ]); + const parsed = raw ? JSON.parse(raw) : []; + const names = parsed.map((entry) => entry.name); + // gh caps the response at --limit with no cursor we can follow here, so a + // full page means the org has at least that many repos and a glob may have + // silently missed some. Surface it instead of resolving an incomplete set. + return { names, truncated: names.length >= ORG_REPO_LIST_LIMIT }; +} + +// Resolve an `extraRepos` spec list into concrete `owner/name` strings. +// Plain `owner/name` entries pass through unchanged. Entries whose name +// segment contains `*` are treated as globs and resolved against the org's +// non-archived repos via `gh repo list` (each org listed at most once). +// Returns { repos, warnings } — warnings are emitted for malformed entries +// or orgs that cannot be listed, so the caller can surface them on stderr. +export function resolveExtraRepos(specs) { + const resolved = new Set(); + const warnings = []; + const orgCache = new Map(); + for (const spec of specs) { + // Require exactly two non-empty segments. Splitting with a limit of 2 would + // silently truncate "owner/group/name" to "owner/group"; reject it instead. + const parts = typeof spec === "string" ? spec.split("/") : []; + if (parts.length !== 2 || !parts[0] || !parts[1]) { + warnings.push(`Ignoring extraRepos entry "${spec}" (must be owner/name).`); + continue; + } + const [owner, name] = parts; + if (!name.includes("*")) { + resolved.add(`${owner}/${name}`); + continue; + } + if (!orgCache.has(owner)) { + try { + const { names, truncated } = listOrgRepos(owner); + orgCache.set(owner, names); + if (truncated) { + warnings.push( + `Repo list for "${owner}" hit the ${ORG_REPO_LIST_LIMIT}-repo cap; some glob matches may be missing.`, + ); + } + } catch (e) { + warnings.push(`Could not list repos for "${owner}": ${ghErrorReason(e)}`); + orgCache.set(owner, []); + } + } + const pattern = name + .replace(/[.+?^${}()|[\]\\]/g, "\\$&") + .replace(/\*/g, ".*"); + const re = new RegExp(`^${pattern}$`); + for (const repoName of orgCache.get(owner)) { + if (re.test(repoName)) resolved.add(`${owner}/${repoName}`); + } + } + return { repos: [...resolved], warnings }; +} + function loadPods(mode, pod) { return mode === "my" ? (pod ? [loadTeam(pod)] : discoverPods()) @@ -220,7 +301,43 @@ export function collectPRActivity({ mode = "team", pod = null, authorScope = "an const rosterLogins = enforceAuthorScope ? new Set(pods.flatMap((p) => [...p.leads, ...p.members])) : null; - const { allPRs, pageNum } = fetchOpenPRs(repoConfig); + + // extraRepos are honored only in team mode. There the pod is treated as the + // sole owner of each extra repo, so every open PR is in scope regardless of + // touched paths. review/my modes stay on the primary repo only. + const extraRepoSpecs = + mode === "team" + ? [...new Set(pods.flatMap((p) => p.extraRepos || []))] + : []; + const { repos: extraRepoList, warnings: repoWarnings } = resolveExtraRepos(extraRepoSpecs); + for (const warning of repoWarnings) console.error(warning); + + const repoTargets = [{ ...repoConfig, soleOwner: false, isPrimary: true }]; + for (const full of extraRepoList) { + if (full === repoConfig.repo) continue; + const { owner, name, repo } = splitRepo(full); + repoTargets.push({ owner, name, repo, soleOwner: true, isPrimary: false }); + } + + const allPRs = []; + const scannedRepos = []; + let pageNum = 0; + for (const target of repoTargets) { + try { + const { allPRs: prs, pageNum: pages } = fetchOpenPRs(target); + pageNum += pages; + for (const pr of prs) { + pr.repo = target.repo; + pr.isPrimaryRepo = target.isPrimary; + pr.soleOwner = target.soleOwner; + } + allPRs.push(...prs); + scannedRepos.push(target.repo); + } catch (e) { + console.error(`Skipping ${target.repo}: ${ghErrorReason(e)}`); + } + } + const isCrossPodMy = mode === "my" && pod === null; const relevantPRs = []; const excludedPRs = []; @@ -230,16 +347,19 @@ export function collectPRActivity({ mode = "team", pod = null, authorScope = "an if (!pr.author?.login) continue; if (mode === "my" && pr.author.login !== currentUser) continue; const files = pr.files?.nodes || []; - if (!isCrossPodMy && !touchesOwnedPaths(files, ownedPaths)) continue; + if (!isCrossPodMy && !pr.soleOwner && !touchesOwnedPaths(files, ownedPaths)) continue; const reviews = pr.reviews?.nodes || []; const reviewState = getReviewState(reviews); const ready = readySince(pr); + const prRef = pr.isPrimaryRepo === false ? `${pr.repo}#${pr.number}` : `#${pr.number}`; const enriched = { ...pr, files, reviewState, ready, stale: now - new Date(ready).getTime() > staleMs, + repo: pr.repo, + prRef, }; if (enforceAuthorScope && !rosterLogins.has(pr.author.login)) { excludedPRs.push(enriched); @@ -253,6 +373,7 @@ export function collectPRActivity({ mode = "team", pod = null, authorScope = "an return { config, repo: repoConfig.repo, + repos: scannedRepos, staleDays, currentUser, pods, @@ -272,12 +393,16 @@ export function classifyTeamPRs(state) { (pr) => !isFullyApprovedInPod(pr, state.roles), ); const reReviewPRs = needsAction.filter((pr) => needsMyReReview(pr, me)); - const reReviewSet = new Set(reReviewPRs.map((pr) => pr.number)); + // Key on the repo-qualified prRef, not the bare number: PR numbers are not + // unique once the dashboard spans multiple repos (extraRepos), so a bare + // number would let a re-review PR mask a same-numbered stale/active PR in a + // different repo and silently drop it from every section. + const reReviewSet = new Set(reReviewPRs.map((pr) => pr.prRef ?? `#${pr.number}`)); const stalePRs = needsAction.filter( - (pr) => pr.stale && !reReviewSet.has(pr.number), + (pr) => pr.stale && !reReviewSet.has(pr.prRef ?? `#${pr.number}`), ); const activePRs = needsAction.filter( - (pr) => !pr.stale && !reReviewSet.has(pr.number), + (pr) => !pr.stale && !reReviewSet.has(pr.prRef ?? `#${pr.number}`), ); const conflictCount = needsAction.filter( (pr) => pr.mergeable === "CONFLICTING", @@ -365,6 +490,8 @@ export function classifyMyPRs(state) { export function toJsonablePR(pr) { return { number: pr.number, + repo: pr.repo, + prRef: pr.prRef, title: pr.title, url: pr.url, author: pr.author, diff --git a/.cursor/skills/_lib/pr-skills/pr-status.mjs b/.cursor/skills/_lib/pr-skills/pr-status.mjs index c113b223ec..432269ffa0 100644 --- a/.cursor/skills/_lib/pr-skills/pr-status.mjs +++ b/.cursor/skills/_lib/pr-skills/pr-status.mjs @@ -84,7 +84,7 @@ function formatTarget(target) { function renderPRLine(pr, podRoles = state.roles, extras = []) { const extraList = Array.isArray(extras) ? extras : extras ? [extras] : []; const lines = [ - `#${pr.number} ${pr.title}`, + `${pr.prRef ?? `#${pr.number}`} ${pr.title}`, pr.url, `by ${pr.author.name || pr.author.login} · ${formatAge(pr.ready)} old`, ]; @@ -134,7 +134,37 @@ function jsonPRs(prs) { function renderExcludedLine(pr) { const author = pr.author.name || pr.author.login; - return ` #${pr.number} — ${pr.title}\n ${pr.url}\n by ${author} (@${pr.author.login}) · ${formatAge(pr.ready)} old`; + const ref = pr.prRef ?? `#${pr.number}`; + return ` ${ref} — ${pr.title}\n ${pr.url}\n by ${author} (@${pr.author.login}) · ${formatAge(pr.ready)} old`; +} + +// Per-repo render cap for the Excluded section. With sole-owner extraRepos, +// every non-roster (incl. bot) PR is excluded, so a busy extra repo could +// otherwise bury the rest. The cap is display-only — --json always carries +// the complete list. +const EXCLUDED_RENDER_CAP_PER_REPO = 10; + +function printExcludedSection(excludedPRs, primaryRepo) { + console.log("⏭️ EXCLUDED (author outside roster)"); + console.log("─".repeat(60)); + const byRepo = new Map(); + for (const pr of excludedPRs) { + const key = pr.repo ?? primaryRepo; + if (!byRepo.has(key)) byRepo.set(key, []); + byRepo.get(key).push(pr); + } + for (const [repo, prs] of byRepo) { + for (const pr of prs.slice(0, EXCLUDED_RENDER_CAP_PER_REPO)) { + console.log(""); + console.log(renderExcludedLine(pr)); + } + const hidden = prs.length - EXCLUDED_RENDER_CAP_PER_REPO; + if (hidden > 0) { + console.log(""); + console.log(` … +${hidden} more in ${repo} — use --json for the full list`); + } + } + console.log(""); } function modeTeam() { @@ -144,6 +174,7 @@ function modeTeam() { console.log(JSON.stringify({ mode, repo: state.repo, + repos: state.repos, currentUser: state.currentUser, staleDays: state.staleDays, authorScope: state.authorScope, @@ -164,6 +195,12 @@ function modeTeam() { }, null, 2)); return; } + const extraRepos = (state.repos ?? []).filter((r) => r !== state.repo); + if (extraRepos.length > 0) { + console.log( + `Repos: ${state.repo} (primary) + ${extraRepos.length} extra: ${extraRepos.join(", ")}\n`, + ); + } const conflictNote = groups.conflictCount > 0 ? ` · ${groups.conflictCount} ⚠️ merge conflicts` : ""; @@ -177,11 +214,7 @@ function modeTeam() { printSection(`🔴 STALE (>${state.staleDays}d)`, groups.stalePRs, renderPRLine); printSection("🟡 NEEDS REVIEW", groups.activePRs, renderPRLine); if (state.authorScope === "pod" && excludedPRs.length > 0) { - printSection( - "⏭️ EXCLUDED (touches pod paths · author outside roster)", - excludedPRs, - renderExcludedLine, - ); + printExcludedSection(excludedPRs, state.repo); } if (groups.needsAction.length === 0) { console.log("All clear — every PR has team + lead approval."); diff --git a/.cursor/skills/_lib/pr-skills/team.mjs b/.cursor/skills/_lib/pr-skills/team.mjs index 2c91ebd0c6..adf8896847 100644 --- a/.cursor/skills/_lib/pr-skills/team.mjs +++ b/.cursor/skills/_lib/pr-skills/team.mjs @@ -50,6 +50,9 @@ export function loadTeam(pod) { assertStringArray(parsed.leads, "leads", teamFile); assertStringArray(parsed.members, "members", teamFile); assertStringArray(parsed.ownedPaths, "ownedPaths", teamFile); + if (parsed.extraRepos !== undefined) { + assertStringArray(parsed.extraRepos, "extraRepos", teamFile); + } if (parsed.leads.length === 0 && parsed.members.length === 0) { console.error(`Warning: ${teamFile} has no leads or members`); } @@ -59,6 +62,7 @@ export function loadTeam(pod) { leads: parsed.leads, members: parsed.members, ownedPaths: parsed.ownedPaths, + extraRepos: Array.isArray(parsed.extraRepos) ? parsed.extraRepos : [], teamFile, }; } diff --git a/.cursor/skills/qv-devops-pr-status/SKILL.md b/.cursor/skills/qv-devops-pr-status/SKILL.md index 080ada7514..68eafb2b37 100644 --- a/.cursor/skills/qv-devops-pr-status/SKILL.md +++ b/.cursor/skills/qv-devops-pr-status/SKILL.md @@ -8,6 +8,14 @@ disable-model-invocation: true Thin wrapper over the shared pr-skills library, pinned to the DevOps pod and scoped to PRs authored by DevOps roster members (`leads ∪ members` in [.github/teams/devops.json](.github/teams/devops.json)). +The dashboard spans the qvac monorepo (filtered by the pod's `ownedPaths`) **plus** every repo declared under `extraRepos` in [.github/teams/devops.json](.github/teams/devops.json). For extra repos the pod is treated as the sole owner — every open PR there is in-scope regardless of touched paths. The monorepo (`tetherto/qvac`) is the primary repo and stays path-filtered; it is intentionally NOT listed under `extraRepos`. Today's `extraRepos` are an explicit curated list: + +- Ops: `tetherto/github-ops`, `tetherto/oss-actions`, `tetherto/qvac-actions`, `tetherto/qvac-devops`, `tetherto/qvac-testops`, `tetherto/release-ops`, `tetherto/data-github-ops` +- Dev: `tetherto/qvac-workbench`, `tetherto/qvac-internal`, `tetherto/qvac-test-suite`, `tetherto/qvac-registry-vcpkg`, `tetherto/qvac-ext-lib-whisper.cpp`, `tetherto/qvac-ext-stable-diffusion.cpp`, `tetherto/qvac-fabric-llm.cpp`, `tetherto/qvac-ext-ggml`, `tetherto/qvac-ext-bergamot-translator`, `tetherto/qvac-ext-marian-dev` +- Research: `tetherto/qvac-research-tool-call`, `tetherto/qvac-research-medpsy`, `tetherto/qvac-research-translations-nmt`, `tetherto/qvac-research-evaluate`, `tetherto/qvac-research-synthetic-data-creation`, `tetherto/qvac-research-model-training`, `tetherto/qvac-model-tools`, `tetherto/qvac-rnd-fabric-llm-bitnet`, `tetherto/qvac-rnd-fabric-llm-finetune` + +`extraRepos` entries are plain `owner/name` strings used as-is. Glob entries (an `owner/name` whose name segment contains `*`, e.g. `tetherto/qvac-*`) are also supported and resolved dynamically per run via `gh repo list --no-archived` — add one to the list if you'd rather track every matching repo automatically instead of curating. Any repo the script cannot read is skipped with a one-line warning on stderr. + ## When to use this skill **Use when:** @@ -20,8 +28,8 @@ Thin wrapper over the shared pr-skills library, pinned to the DevOps pod and sco ## Prerequisites - `gh` CLI installed and authenticated (`gh auth status`) -- User must have access to `tetherto/qvac` repository -- Team roster maintained at [.github/teams/devops.json](.github/teams/devops.json) +- User must have read access to `tetherto/qvac` AND every repo declared under `extraRepos` (any repo the script cannot read is skipped with a one-line warning on stderr) +- Team roster + `extraRepos` maintained at [.github/teams/devops.json](.github/teams/devops.json) ## Usage @@ -32,13 +40,17 @@ node .cursor/skills/_lib/pr-skills/pr-status.mjs --pod devops --mode team --auth | tee "/tmp/devops-pr-status-${DATE}.txt" ``` -`--authors pod` restricts the main dashboard to PRs authored by DevOps roster members. PRs that touch DevOps-owned paths but are authored outside the roster are surfaced in a separate "Excluded" section at the bottom of the same dashboard, so the pod still has visibility into cross-pod work hitting its paths without those PRs polluting the queue. See [.cursor/skills/_lib/pr-skills/README.md](.cursor/skills/_lib/pr-skills/README.md) for the flag's full behavior. +`--authors pod` restricts the main dashboard to PRs authored by DevOps roster members. PRs that touch DevOps-owned paths (in the monorepo) or live in any extra repo but are authored outside the roster are surfaced in a separate "Excluded" section at the bottom of the same dashboard, so the pod still has visibility into cross-pod work hitting its surfaces without those PRs polluting the queue. See [.cursor/skills/_lib/pr-skills/README.md](.cursor/skills/_lib/pr-skills/README.md) for the flag's full behavior. + +`extraRepos` is honored only by `--mode team`. `--mode review` and `--mode my` continue to operate against the configured primary repo only (the monorepo); cross-repo personal review/my-PR dashboards are not in scope of this skill. + +The first line of the dashboard is a `Repos:` summary listing the primary repo plus every extra repo that contributed to the run, so the user can see the full scope at a glance. PRs from extra repos render as `owner/repo#` (e.g. `tetherto/qvac-workbench#42`); PRs from the primary monorepo render as bare `#` exactly as before. The same prefix shows in the Excluded section. -For the personal review queue scoped to DevOps PRs, use `--mode review` (without `--authors pod` — review queue intentionally includes cross-pod authors whose review the user owes). +For the personal review queue scoped to DevOps PRs, use `--mode review` (without `--authors pod` — review queue intentionally includes cross-pod authors whose review the user owes). That mode stays on the primary repo. ## Workflow -1. Run the script with `--pod devops --mode team --authors pod`, **teeing stdout to `/tmp/devops-pr-status-.txt`** so the dashboard is available for paste afterwards. Redirect stderr to a sibling `.stderr` file (it contains progress / `SLACK_VALIDATION_REQUIRED` notices, not dashboard content). +1. Run the script with `--pod devops --mode team --authors pod`, **teeing stdout to `/tmp/devops-pr-status-.txt`** so the dashboard is available for paste afterwards. Redirect stderr to a sibling `.stderr` file (it contains progress / `SLACK_VALIDATION_REQUIRED` notices and any skipped-repo warnings, not dashboard content). 2. Present the dashboard to the user in the **chat presentation format** (see below) — not as a raw paste of the script output. 3. Surface the summary header counts (need your re-review / stale / merge conflicts / excluded) prominently. 4. **Print the paste-ready copy commands.** The dashboard at `/tmp/devops-pr-status-.txt` is plain text with two-space indent — when pasted into a Slack thread, Slack auto-renders the indented lines as nested bullets and turns `#` into PR auto-links. No re-formatting is needed. @@ -58,7 +70,7 @@ The in-chat rendering uses Markdown with hyperlinked PR numbers. This is distinc Required layout (in this exact order): 1. **Title line** — `## DevOps Pod — PR Status (authors scoped to roster)`. -2. **Headline summary** — one bold line restating the script summary counts (`N PRs need attention · X fully approved · Y need your re-review · Z stale`). +2. **Headline summary** — one bold line restating the script summary counts (`N PRs need attention · X fully approved · Y need your re-review · Z stale`). Append `· repos scanned` when the script's `Repos:` line lists more than just the primary repo (i.e., `extraRepos` resolved to at least one repo), so the user can see the scope at a glance. 3. **Roster line** — one-line listing of the roster: ``` Roster: `Proletter` (lead) + `darkynt`, `GSServita`, `sidj-thr`, `tamer-hassan-tether`, `yauhenipankratovich-web`. @@ -74,10 +86,11 @@ Required layout (in this exact order): Bullet format for the active sections (Stale / Needs Review / Re-review): ``` -- [#]() — · `<author-login>` · <age> · <approvals/notes> · **<blockers/labels>** +- [<ref>](<url>) — <title> · `<author-login>` · <age> · <approvals/notes> · **<blockers/labels>** ``` -- `[#<num>](<url>)` — Markdown link, never bare `#<num>`. +- `<ref>` is `#<num>` for PRs in the primary monorepo and `owner/repo#<num>` (e.g. `tetherto/qvac-workbench#42`) for PRs from any `extraRepos` entry. Mirror the script's `prRef` form 1:1 — the rendered link text must match what appears in the dashboard at `/tmp/devops-pr-status-<DATE>.txt`. +- `[<ref>](<url>)` — Markdown link, never bare `<ref>`. - `<title>` is the PR title verbatim, no truncation. - `<author-login>` is wrapped in backticks. - `<age>` is the script's age string (e.g., `4d 13h`). @@ -87,7 +100,7 @@ Bullet format for the active sections (Stale / Needs Review / Re-review): Bullet format for the Excluded section (compact — these are not the pod's review queue): ``` -- [#<num>](<url>) `<author-login>` +- [<ref>](<url>) `<author-login>` ``` ## References diff --git a/.github/teams/devops.json b/.github/teams/devops.json index 1ee67bfe51..314bae983e 100644 --- a/.github/teams/devops.json +++ b/.github/teams/devops.json @@ -13,5 +13,33 @@ ".github/actions/", ".github/scripts/", "scripts/" + ], + "extraRepos": [ + "tetherto/github-ops", + "tetherto/oss-actions", + "tetherto/qvac-actions", + "tetherto/qvac-devops", + "tetherto/qvac-testops", + "tetherto/release-ops", + "tetherto/data-github-ops", + "tetherto/qvac-workbench", + "tetherto/qvac-internal", + "tetherto/qvac-test-suite", + "tetherto/qvac-registry-vcpkg", + "tetherto/qvac-ext-lib-whisper.cpp", + "tetherto/qvac-ext-stable-diffusion.cpp", + "tetherto/qvac-fabric-llm.cpp", + "tetherto/qvac-ext-ggml", + "tetherto/qvac-ext-bergamot-translator", + "tetherto/qvac-ext-marian-dev", + "tetherto/qvac-research-tool-call", + "tetherto/qvac-research-medpsy", + "tetherto/qvac-research-translations-nmt", + "tetherto/qvac-research-evaluate", + "tetherto/qvac-research-synthetic-data-creation", + "tetherto/qvac-research-model-training", + "tetherto/qvac-model-tools", + "tetherto/qvac-rnd-fabric-llm-bitnet", + "tetherto/qvac-rnd-fabric-llm-finetune" ] }