From f88b964379ca9ae6c16e1f2360e436683abc0fe5 Mon Sep 17 00:00:00 2001 From: Hatton Date: Sun, 28 Jun 2026 16:16:42 -0600 Subject: [PATCH 1/7] Add PR-status sync scripts for Orca/GitHub/YouTrack alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync-move.mjs: zero-token script to move a single work item to a named status (waiting-ai | in-review | has-comments | completed) across Orca, GitHub board, and YouTrack. Accepts `all ` to sync all three from a PR number, or individual `orca`/`gh`/`yt` subcommands. BL# is resolved from branch → display name → PR title → PR body. sync-pr-status.mjs: polling reconciler (zero tokens) intended to run every 15 min via Orca automation. Reads all Orca worktrees with a linkedPR, fetches GitHub board status in one batch call, and updates any Orca worktrees + YouTrack issues that are out of sync. Co-Authored-By: Claude Sonnet 4.6 --- scripts/sync-move.mjs | 221 +++++++++++++++++++++++++++++++++++++ scripts/sync-pr-status.mjs | 205 ++++++++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 scripts/sync-move.mjs create mode 100644 scripts/sync-pr-status.mjs diff --git a/scripts/sync-move.mjs b/scripts/sync-move.mjs new file mode 100644 index 000000000000..e3b70a10ce0a --- /dev/null +++ b/scripts/sync-move.mjs @@ -0,0 +1,221 @@ +#!/usr/bin/env node +/** + * sync-move.mjs — move a work item to a new status in Orca, GitHub board, and/or YouTrack. + * Zero AI tokens; all mappings are hardcoded. + * + * Usage: + * node scripts/sync-move.mjs orca + * node scripts/sync-move.mjs gh + * node scripts/sync-move.mjs yt + * node scripts/sync-move.mjs all ← syncs all three + * + * Status keys: + * waiting-ai – Waiting for AI review + * in-review – Ready for human review + * has-comments – Reviewer left comments (author needs to act) + * completed – Work done (PR merged/closed) + * + * Env vars required: + * YOUTRACK – YouTrack permanent token (perm-...) + */ + +import { execSync } from "child_process"; + +// ── GitHub project constants ────────────────────────────────────────────────── +const GH_OWNER = "BloomBooks"; +const GH_PROJECT_NUMBER = 2; +const GH_PROJECT_ID = "PVT_kwDOAFlSFM4Bawkp"; +const GH_STATUS_FIELD = "PVTSSF_lADOAFlSFM4BawkpzhVl0_w"; + +// GitHub board single-select option IDs (from `gh project field-list 2 --owner BloomBooks`) +const GH_OPTIONS = { + "waiting-ai": "97860183", // Waiting for AI-Review + "in-review": "05eedb52", // Ready for Human + "has-comments": "99a3f545", // Has Comments + completed: null, // auto-hidden when PR closes +}; + +// YouTrack state names (exact strings from the bundle) +const YT_STATES = { + "waiting-ai": "In Progress", + "in-review": "Ready For Code Review", + "has-comments": "Has Comments", + completed: null, // leave for human (type-dependent: bug→Closed, feature→Ready For Testing) +}; + +// Orca workspace-status IDs +const ORCA_STATUSES = { + "waiting-ai": "status-5", + "in-review": "in-review", + "has-comments": "status-6", + completed: "completed", +}; + +const YT_BASE = "https://issues.bloomlibrary.org/youtrack"; +const YT_TOKEN = process.env.YOUTRACK; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function run(cmd) { + return execSync(cmd, { encoding: "utf8" }).trim(); +} + +function validateStatus(status) { + if (!(status in GH_OPTIONS)) { + console.error( + `Unknown status "${status}". Valid: ${Object.keys(GH_OPTIONS).join(" | ")}`, + ); + process.exit(1); + } +} + +// ── Per-system move functions ───────────────────────────────────────────────── + +async function moveOrca(worktreePath, status) { + const s = ORCA_STATUSES[status]; + run( + `orca worktree set --worktree path:${worktreePath} --workspace-status ${s} --json`, + ); + console.log(`✓ Orca: ${worktreePath} → ${s}`); +} + +async function moveGh(prNumber, status) { + const optionId = GH_OPTIONS[status]; + if (!optionId) { + console.log( + ` GH: no board move for "${status}" (auto-handled by GitHub)`, + ); + return; + } + + // Look up the board item ID for this PR number + const raw = run( + `gh project item-list ${GH_PROJECT_NUMBER} --owner ${GH_OWNER} --format json --limit 200`, + ); + const data = JSON.parse(raw); + const item = data.items.find((i) => i.content?.number === Number(prNumber)); + if (!item) { + console.warn(` GH: PR #${prNumber} not found on board — skipping`); + return; + } + + run( + `gh project item-edit --project-id ${GH_PROJECT_ID} --id ${item.id} --field-id ${GH_STATUS_FIELD} --single-select-option-id ${optionId}`, + ); + console.log(`✓ GH board: PR #${prNumber} (${item.id}) → ${status}`); +} + +async function moveYt(issueId, status) { + const state = YT_STATES[status]; + if (!state) { + console.log(` YT: no move for "${status}" — leave for human judgment`); + return; + } + if (!YT_TOKEN) throw new Error("YOUTRACK env var not set"); + + const res = await fetch(`${YT_BASE}/api/commands`, { + method: "POST", + headers: { + Authorization: `Bearer ${YT_TOKEN}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + query: `State ${state}`, + issues: [{ idReadable: issueId }], + }), + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`YouTrack ${res.status}: ${err}`); + } + console.log(`✓ YouTrack: ${issueId} → ${state}`); +} + +// ── "all" — move all three systems from a PR number ─────────────────────────── + +async function moveAll(prNumber, status) { + // 1. Find the Orca worktree linked to this PR + const wtRaw = run(`orca worktree list --json`); + const wtData = JSON.parse(wtRaw); + const wt = wtData.result.worktrees.find( + (w) => w.linkedPR === Number(prNumber), + ); + + // 2. Extract BL-xxxxx from branch name, display name, or PR title (in that order) + let ytIssue = null; + if (wt) { + const branch = wt.git?.branch ?? ""; + const name = wt.displayName ?? ""; + const m = (branch + " " + name).match(/BL-\d+/); + if (m) ytIssue = m[0]; + } + if (!ytIssue) { + try { + // Search PR title then body for BL-xxxxx + const pr = run( + `gh pr view ${prNumber} --repo ${GH_OWNER}/BloomDesktop --json title,body`, + ); + const { title = "", body = "" } = JSON.parse(pr); + const m = (title + " " + body).match(/BL-\d+/); + if (m) ytIssue = m[0]; + } catch { + /* gh not available or PR not found — skip */ + } + } + + // 3. Move each system + await moveGh(prNumber, status); + + if (wt) { + await moveOrca(wt.path, status); + } else { + console.warn( + ` Orca: no worktree with linkedPR=${prNumber} — skipping`, + ); + } + + if (ytIssue) { + await moveYt(ytIssue, status); + } else { + console.warn( + ` YT: no BL-xxxxx found for PR #${prNumber} — skipping YouTrack`, + ); + } +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +const [, , cmd, target, status] = process.argv; + +if (!cmd || !target || !status) { + console.error( + [ + "Usage: node scripts/sync-move.mjs ", + " cmd: orca | gh | yt | all", + " status: waiting-ai | in-review | has-comments | completed", + "Examples:", + " node scripts/sync-move.mjs orca D:/bloom.worktrees/BL-16467 has-comments", + " node scripts/sync-move.mjs gh 7994 has-comments", + " node scripts/sync-move.mjs yt BL-16467 has-comments", + " node scripts/sync-move.mjs all 7994 has-comments", + ].join("\n"), + ); + process.exit(1); +} + +validateStatus(status); + +const handlers = { orca: moveOrca, gh: moveGh, yt: moveYt, all: moveAll }; +if (!(cmd in handlers)) { + console.error(`Unknown command "${cmd}". Use: orca | gh | yt | all`); + process.exit(1); +} + +try { + await handlers[cmd](target, status); +} catch (e) { + console.error("Error:", e.message); + process.exit(1); +} diff --git a/scripts/sync-pr-status.mjs b/scripts/sync-pr-status.mjs new file mode 100644 index 000000000000..56f248fa57e7 --- /dev/null +++ b/scripts/sync-pr-status.mjs @@ -0,0 +1,205 @@ +#!/usr/bin/env node +/** + * sync-pr-status.mjs — reconcile Orca worktree statuses with the GitHub board. + * Zero AI tokens; all mappings are hardcoded. + * + * Run automatically every 15 minutes via Orca automation, or manually. + * + * Env vars required: + * YOUTRACK – YouTrack permanent token (perm-...) + */ + +import { execSync } from "child_process"; + +const GH_OWNER = "BloomBooks"; +const GH_PROJECT_NUMBER = 2; + +// GitHub board status label → internal key +const GH_STATUS_TO_KEY = { + "Waiting for AI-Review": "waiting-ai", + "Ready for Human": "in-review", + "Has Comments": "has-comments", +}; + +// Internal key → Orca workspaceStatus value +const ORCA_STATUSES = { + "waiting-ai": "status-5", + "in-review": "in-review", + "has-comments": "status-6", + completed: "completed", +}; + +// Internal key → YouTrack state name (null = leave for human) +const YT_STATES = { + "waiting-ai": "In Progress", + "in-review": "Ready For Code Review", + "has-comments": "Has Comments", + completed: null, +}; + +const YT_BASE = "https://issues.bloomlibrary.org/youtrack"; +const YT_TOKEN = process.env.YOUTRACK; + +function run(cmd) { + return execSync(cmd, { encoding: "utf8" }).trim(); +} + +/** Return the first BL-xxxxx found in a string, or null. */ +function extractBl(text) { + const m = (text ?? "").match(/BL-\d+/); + return m ? m[0] : null; +} + +async function setOrcaStatus(worktreePath, statusKey) { + run( + `orca worktree set --worktree path:${worktreePath} --workspace-status ${ORCA_STATUSES[statusKey]}`, + ); +} + +async function setYtState(issueId, statusKey) { + const state = YT_STATES[statusKey]; + if (!state) return false; + if (!YT_TOKEN) { + console.warn(" YT: YOUTRACK env var not set — skipping"); + return false; + } + + const res = await fetch(`${YT_BASE}/api/commands`, { + method: "POST", + headers: { + Authorization: `Bearer ${YT_TOKEN}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + query: `State ${state}`, + issues: [{ idReadable: issueId }], + }), + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`YouTrack ${res.status}: ${err}`); + } + return true; +} + +async function main() { + // 1. Orca worktrees that have a linked PR + const wtRaw = run("orca worktree list --json"); + const wtData = JSON.parse(wtRaw); + const linked = wtData.result.worktrees.filter((w) => w.linkedPR != null); + + if (!linked.length) { + console.log("No Orca worktrees with linkedPR — nothing to sync"); + return; + } + console.log(`Checking ${linked.length} Orca worktree(s) with linked PRs\n`); + + // 2. GitHub board status by PR number (single batch call) + const boardRaw = run( + `gh project item-list ${GH_PROJECT_NUMBER} --owner ${GH_OWNER} --format json --limit 200`, + ); + const boardData = JSON.parse(boardRaw); + const boardByPr = {}; + for (const item of boardData.items) { + if (item.content?.number) { + boardByPr[item.content.number] = item.status; + } + } + + // 3. Process each linked worktree + let changed = 0; + for (const wt of linked) { + const prNum = wt.linkedPR; + const currentOrcaStatus = wt.workspaceStatus; + const label = `PR #${prNum} (${wt.displayName})`; + + let targetKey; + const boardStatus = boardByPr[prNum]; + + if (!boardStatus) { + // Not on board — may be merged/closed; check GitHub directly + try { + const prJson = run( + `gh pr view ${prNum} --repo ${GH_OWNER}/BloomDesktop --json state`, + ); + const { state } = JSON.parse(prJson); + if (state === "MERGED" || state === "CLOSED") { + targetKey = "completed"; + } else { + // Open but not yet on the board — skip + console.log(` ${label}: open, not on board — skipping`); + continue; + } + } catch { + console.warn(` ${label}: gh pr view failed — skipping`); + continue; + } + } else { + targetKey = GH_STATUS_TO_KEY[boardStatus]; + if (!targetKey) { + console.log( + ` ${label}: unrecognized board status "${boardStatus}" — skipping`, + ); + continue; + } + } + + const targetOrcaStatus = ORCA_STATUSES[targetKey]; + if (currentOrcaStatus === targetOrcaStatus) { + console.log(` ${label}: already ${currentOrcaStatus} ✓`); + continue; + } + + // Resolve BL# for YouTrack: branch → display name → PR title/body + let ytIssue = extractBl(wt.git?.branch) ?? extractBl(wt.displayName); + if (!ytIssue) { + try { + const pr = run( + `gh pr view ${prNum} --repo ${GH_OWNER}/BloomDesktop --json title,body`, + ); + const { title = "", body = "" } = JSON.parse(pr); + ytIssue = extractBl(title + " " + body); + } catch { + /* skip */ + } + } + + // Apply Orca update + try { + await setOrcaStatus(wt.path, targetKey); + console.log( + `✓ ${label}: Orca ${currentOrcaStatus} → ${targetOrcaStatus}`, + ); + changed++; + } catch (e) { + console.error(` ${label}: Orca update failed: ${e.message}`); + continue; + } + + // Apply YouTrack update + if (ytIssue) { + try { + const moved = await setYtState(ytIssue, targetKey); + if (moved) + console.log( + ` ✓ YouTrack ${ytIssue} → ${YT_STATES[targetKey]}`, + ); + } catch (e) { + console.error(` YouTrack ${ytIssue}: ${e.message}`); + } + } else { + console.log( + ` YT: no BL-xxxxx for PR #${prNum} — skipping YouTrack`, + ); + } + } + + console.log(`\nDone. ${changed} worktree(s) updated.`); +} + +main().catch((e) => { + console.error("Fatal:", e.message); + process.exit(1); +}); From 1fbcd1c733545fd4a470fe0d7eb7737a16c3f99a Mon Sep 17 00:00:00 2001 From: Hatton Date: Sun, 28 Jun 2026 16:22:47 -0600 Subject: [PATCH 2/7] Add pr-kanban-sync skill; update pr-ready-for-human to use sync scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New skill .github/skills/pr-kanban-sync/SKILL.md documents the full state machine (waiting-ai / in-review / has-comments / completed), maps each state across Orca / GitHub board / YouTrack, and is the canonical reference for sync-move.mjs and sync-pr-status.mjs. Updated pr-ready-for-human to delegate all board/Orca/YouTrack moves to `node scripts/sync-move.mjs all ` instead of raw GraphQL mutations, fixed env var YOUTRACK_TOKEN → YOUTRACK, and fixed re-entry Orca status (in-progress → waiting-ai via the script). Co-Authored-By: Claude Sonnet 4.6 --- .github/skills/pr-kanban-sync/SKILL.md | 125 +++++++++++++++++++++ .github/skills/pr-ready-for-human/SKILL.md | 83 +++++--------- 2 files changed, 150 insertions(+), 58 deletions(-) create mode 100644 .github/skills/pr-kanban-sync/SKILL.md diff --git a/.github/skills/pr-kanban-sync/SKILL.md b/.github/skills/pr-kanban-sync/SKILL.md new file mode 100644 index 000000000000..ddfa59d91cc8 --- /dev/null +++ b/.github/skills/pr-kanban-sync/SKILL.md @@ -0,0 +1,125 @@ +--- +name: pr-kanban-sync +description: State machine and scripts for keeping Orca board, GitHub Projects board, and YouTrack in sync as PRs move through review stages. Use these scripts instead of raw GraphQL or direct CLI calls whenever moving a PR between stages. +argument-hint: "status to move to: waiting-ai | in-review | has-comments | completed" +user-invocable: true +--- + +# PR Kanban Sync + +## Overview + +Three systems track the same PRs. They must stay aligned: + +| State key | Orca column | GitHub board | YouTrack | +|---|---|---|---| +| `waiting-ai` | Waiting on AI Review | Waiting for AI-Review | In Progress | +| `in-review` | In human review | Ready for Human | Ready For Code Review | +| `has-comments` | Has Comments | Has Comments | Has Comments | +| `completed` | completed (archived) | auto-hidden on merge | leave for human | + +## Scripts + +Both scripts live at `scripts/` in the repo root and require Node.js 18+. + +### `sync-move.mjs` — move one item manually + +```bash +# Move all three systems at once (preferred): +node scripts/sync-move.mjs all + +# Individual systems: +node scripts/sync-move.mjs gh +node scripts/sync-move.mjs orca +node scripts/sync-move.mjs yt +``` + +`all` resolves the Orca worktree via `linkedPR` and the YouTrack issue via BL# from branch → display name → PR title → PR body. If either lookup fails it warns and skips that system; use the individual subcommands as fallback. + +Requires env var `YOUTRACK` (YouTrack permanent token). + +### `sync-pr-status.mjs` — polling reconciler (zero tokens) + +```bash +node scripts/sync-pr-status.mjs +``` + +Reads all Orca worktrees with `linkedPR` set, fetches GitHub board status in one batch call, and updates any Orca worktrees + YouTrack issues that are out of sync. Intended to run every 15 minutes via Orca automation (see **Automation** below). Also safe to run manually to force a re-sync. + +## State Machine + +``` +[PR opened / first push] + → node scripts/sync-move.mjs all waiting-ai + +[Devin and CI finish cleanly → ready for human] + → node scripts/sync-move.mjs all in-review + +[Human reviewer: CHANGES_REQUESTED] + → node scripts/sync-move.mjs all has-comments + +[Author pushes new commits while in has-comments or in-review] + → node scripts/sync-move.mjs all waiting-ai (restart bot-wait) + +[PR merged or closed] + → node scripts/sync-move.mjs all completed + (YouTrack is skipped for completed — type-dependent, leave for human) +``` + +## GitHub Board Reference + +Project: **PR Review Tracker** — https://github.com/orgs/BloomBooks/projects/2 + +GraphQL IDs (hardcoded in `sync-move.mjs` — do not re-query unless they stop working): +- Project ID: `PVT_kwDOAFlSFM4Bawkp` +- Status field ID: `PVTSSF_lADOAFlSFM4BawkpzhVl0_w` +- Option IDs: `97860183` (Waiting for AI-Review), `05eedb52` (Ready for Human), `99a3f545` (Has Comments) + +## Adding a PR to the Board + +`sync-move.mjs` only updates items already on the board. To add a new PR: + +```bash +PR_NODE_ID=$(gh pr view --repo BloomBooks/BloomDesktop --json id --jq '.id') +gh api graphql -f query="mutation { + addProjectV2ItemById(input: { + projectId: \"PVT_kwDOAFlSFM4Bawkp\" + contentId: \"$PR_NODE_ID\" + }) { item { id } } +}" +# Then immediately set its status: +node scripts/sync-move.mjs gh waiting-ai +``` + +## Automation (15-minute polling) + +Register once per machine (the automation targets the `master-2` worktree as a stable shell host): + +```powershell +orca automations create ` + --name "Sync PR Status" ` + --trigger "*/15 * * * *" ` + --workspace "path:D:/orca-worktrees/bloom/master-2" ` + --reuse-session ` + --prompt "Run this command and report its output: node scripts/sync-pr-status.mjs" ` + --provider claude ` + --json +``` + +To list or remove: +```bash +orca automations list --json +orca automations remove --json +``` + +## Known Limitations + +- `all` and the polling script only update Orca if `linkedPR` is set on the worktree. Orca sets this automatically when a PR is opened through its UI. If you created the PR via `gh pr create`, link it manually: + ```bash + # Not yet supported by orca CLI — use individual subcommands as fallback + node scripts/sync-move.mjs gh + orca worktree set --worktree active --workspace-status + node scripts/sync-move.mjs yt + ``` +- YouTrack is skipped if no `BL-xxxxx` appears anywhere in the branch name, display name, PR title, or PR body. +- The `completed` transition leaves YouTrack alone (bug vs feature determines the final state there). diff --git a/.github/skills/pr-ready-for-human/SKILL.md b/.github/skills/pr-ready-for-human/SKILL.md index 48c270446f1f..b6e32b535735 100644 --- a/.github/skills/pr-ready-for-human/SKILL.md +++ b/.github/skills/pr-ready-for-human/SKILL.md @@ -12,16 +12,14 @@ When the user says **"This is ready for human review"** (or equivalent), execute When the user pushes a new commit to a PR that is already in a review state, re-enter at **Stage 4** (reset to bot-wait). -## GitHub Project Board Reference -Project: **PR Review Tracker** — https://github.com/orgs/BloomBooks/projects/2 +## Board & Sync Reference +All Orca / GitHub board / YouTrack moves use the scripts documented in the `pr-kanban-sync` skill. Status keys: `waiting-ai` | `in-review` | `has-comments` | `completed`. -GraphQL IDs (hardcoded — do not re-query unless these stop working): -- Project ID: `PVT_kwDOAFlSFM4Bawkp` -- Status field ID: `PVTSSF_lADOAFlSFM4BawkpzhVl0_w` -- Status option IDs: - - `97860183` → "Waiting for AI-Review" - - `99a3f545` → "Has Comments" - - `05eedb52` → "Ready for Human" +```bash +node scripts/sync-move.mjs all +``` + +See `.github/skills/pr-kanban-sync/SKILL.md` for IDs, limitations, and individual-system fallbacks. ## Stage 1: Committed & Pushed @@ -66,54 +64,34 @@ curl -s -X POST "https://issues.bloomlibrary.org/youtrack/api/issues// -d "{\"text\": \"PR: \"}" ``` -If `$YOUTRACK_TOKEN` is not set, tell the user: "I need a YouTrack API token to post the PR link. Set `YOUTRACK_TOKEN` in your environment or post the comment manually: PR: " +If `$YOUTRACK` is not set, tell the user: "I need a YouTrack API token to post the PR link. Set `YOUTRACK` in your environment or post the comment manually: PR: " ## Stage 4: GitHub Project Board — Add to Project & Set "Waiting for AI-Review" -### Find or add the PR's project item +### Ensure the PR is on the board +Check if it's already there: ```bash -gh api graphql -f query='{ - repository(owner:"BloomBooks", name:"BloomDesktop") { - pullRequest(number:) { - projectItems(first:10) { - nodes { id project { number } } - } - } - } -}' +gh project item-list 2 --owner BloomBooks --format json --limit 200 | \ + node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.items.find(i=>i.content?.number===)?.id ?? 'not found')" ``` -If the PR is not in project 2, add it: +If not on the board, add it: ```bash -gh api graphql -f query='mutation { +PR_NODE_ID=$(gh pr view --repo BloomBooks/BloomDesktop --json id --jq '.id') +gh api graphql -f query="mutation { addProjectV2ItemById(input: { - projectId: "PVT_kwDOAFlSFM4Bawkp" - contentId: "" + projectId: \"PVT_kwDOAFlSFM4Bawkp\" + contentId: \"$PR_NODE_ID\" }) { item { id } } -}' -``` - -Get the PR node ID with: -```bash -gh pr view --repo BloomBooks/BloomDesktop --json id --jq '.id' +}" ``` -### Set Status to "Waiting for AI-Review" +### Set all three systems to "waiting-ai" ```bash -gh api graphql -f query='mutation { - updateProjectV2ItemFieldValue(input: { - projectId: "PVT_kwDOAFlSFM4Bawkp" - itemId: "" - fieldId: "PVTSSF_lADOAFlSFM4BawkpzhVl0_w" - value: { singleSelectOptionId: "97860183" } - }) { projectV2Item { id } } -}' +node scripts/sync-move.mjs all waiting-ai ``` -### Also reset Orca card to "in-progress" if re-entering after a commit -```bash -orca worktree set --worktree active --workspace-status in-progress --json -``` +This sets GitHub board → "Waiting for AI-Review", Orca → "status-5" (Waiting on AI Review), YouTrack → "In Progress". If `all` warns about a missing Orca link, fall back to individual commands (see `pr-kanban-sync` skill). ## Stage 5: Bot-Review Wait Cycle @@ -153,29 +131,18 @@ If the user says "skip bots, just move it to human review" → honor explicitly ## Stage 6: Move to "Ready for Human" -### GitHub project board ```bash -gh api graphql -f query='mutation { - updateProjectV2ItemFieldValue(input: { - projectId: "PVT_kwDOAFlSFM4Bawkp" - itemId: "" - fieldId: "PVTSSF_lADOAFlSFM4BawkpzhVl0_w" - value: { singleSelectOptionId: "05eedb52" } - }) { projectV2Item { id } } -}' +node scripts/sync-move.mjs all in-review ``` -### Orca board -```bash -orca worktree set --worktree active --workspace-status in-review --json -``` +This sets GitHub board → "Ready for Human", Orca → "in-review", YouTrack → "Ready For Code Review". ### Report "PR # is now in **Ready for Human** review. PR: " ## Re-entry After a New Commit When a commit is pushed to a PR already in "Ready for Human" or "Has Comments": -1. Re-run Stage 4 (set project card back to "Waiting for AI-Review", set Orca to "in-progress"). +1. Re-run Stage 4 (`node scripts/sync-move.mjs all waiting-ai` — resets all three systems). 2. Re-run Stages 5–6 (new bot-wait cycle, then back to human when quiet). The whole cycle must restart because bots re-run on every commit. @@ -185,5 +152,5 @@ The whole cycle must restart because bots re-run on every commit. - Never move to "Ready for Human" without Devin having run. - Always check for duplicate YouTrack comments before posting. - Always post Devin findings as GitHub PR comments before moving to any review state. -- If `YOUTRACK_TOKEN` is unavailable, note it and continue — the YouTrack comment is the lowest-stakes step. +- If `YOUTRACK` is unavailable, note it and continue — the YouTrack comment is the lowest-stakes step. - If the Orca browser is unavailable for Devin, tell the user what URL to open manually. From 5723f76f17c8f91b3bd56eb710b80ca1b0f6436c Mon Sep 17 00:00:00 2001 From: Hatton Date: Mon, 29 Jun 2026 13:50:03 -0600 Subject: [PATCH 3/7] Update PR review skills: revise devin-review, replace reviewable-thread-replies with reviewable-comments - devin-review: pin to the correct PR tab, detect stale/Outdated results, read finding buttons instead of grepping innerText, and note there is no GitHub-side completion signal. - Replace the reviewable-thread-replies skill with a broader reviewable-comments skill. --- .github/skills/devin-review/SKILL.md | 45 ++++- .github/skills/reviewable-comments/SKILL.md | 167 ++++++++++++++++++ .../skills/reviewable-thread-replies/SKILL.md | 155 ---------------- 3 files changed, 206 insertions(+), 161 deletions(-) create mode 100644 .github/skills/reviewable-comments/SKILL.md delete mode 100644 .github/skills/reviewable-thread-replies/SKILL.md diff --git a/.github/skills/devin-review/SKILL.md b/.github/skills/devin-review/SKILL.md index c3b078d88884..9492bf4cc140 100644 --- a/.github/skills/devin-review/SKILL.md +++ b/.github/skills/devin-review/SKILL.md @@ -70,19 +70,52 @@ chrome-devtools new_page "https://app.devin.ai/review///pull/ tab +chrome-devtools select_page # pin future calls to it +chrome-devtools evaluate_script "() => (document.body.innerText.match(/PR #\d+/)||['?'])[0]" +# → must print "PR #". If not, you are about to read the wrong review. +``` + +Close stray tabs when done to avoid accumulating isolated-context tabs. + +### 2. Check if Review is Complete — beware STALE results + +Reviews take a while (**10–20+ minutes**), and a **new push invalidates the prior review**: +Devin keeps showing the *old* commit's findings, prefixed with an **"Outdated"** badge, until +the re-review of the new HEAD finishes. So "I see `1 Bug` / `6 Flags`" does **NOT** mean the +results are current — you must check they are not marked Outdated. + +Do **not** grep `document.body.innerText` for status. The page embeds the entire diff inline, +so keywords like `pending`, `complete`, `reviewing`, and even commit SHAs match diff/source +text and give false positives. Read the finding **buttons** in the sidebar instead: ```bash -chrome-devtools evaluate_script "() => document.body.innerText" 2>/dev/null | grep -E "Bug|Flags" +chrome-devtools evaluate_script "() => { const b=[...document.querySelectorAll('button')]; const headers=b.filter(x=>/\d+\s+(Bug|Flag)s?/.test(x.textContent)).map(x=>x.textContent.trim().replace(/\s+/g,' ')); return JSON.stringify(headers); }" ``` -- If you see lines like `1 Bug` or `6 Flags` → review is complete. Proceed to step 3. -- If the page shows only a loading state or no Bug/Flags section → review is not yet done. Report "Devin review not yet complete" and return. Come back in 5–10 minutes. +- Headers like `"1 Bug"`, `"6 Flags"` with **no** `Outdated` prefix → review is current for the + selected commit. Proceed to step 3. +- Headers like `"Outdated 1 Bug"`, `"Outdated6 Flags"` → these are the *previous* commit's + results; the re-review is still running. Report "Devin re-review still in progress" and come + back in 5–10 minutes. Re-checking requires a reload (`chrome-devtools navigate_page --type reload`). +- No Bug/Flags buttons at all → review hasn't produced results yet; come back later. - Timeout after 30 minutes total from trigger. -The review typically takes 10–20 minutes from first page navigation. +**There is no GitHub-side signal.** Devin does not post a commit status/check, so `gh pr checks` +will not tell you whether the review is done — the web UI is the only source. (A "pending" +entry in Devin's commit-status popup is some *other* GitHub check, e.g. `code-review/reviewable`, +not Devin.) + +**Do not pre-judge the findings.** Even when a push looks purely mechanical (e.g. a master +merge), wait for the non-Outdated results and enumerate them — don't assume the prior set +carries over. ### 3. Enumerate Findings diff --git a/.github/skills/reviewable-comments/SKILL.md b/.github/skills/reviewable-comments/SKILL.md new file mode 100644 index 000000000000..3ab60db31ed5 --- /dev/null +++ b/.github/skills/reviewable-comments/SKILL.md @@ -0,0 +1,167 @@ +--- +name: reviewable-thread-replies +description: 'Reply to Reviewable PR discussion threads using the reviewable CLI. Use whenever the user asks you to respond to review comments.' +argument-hint: 'Repo/PR reference (e.g. BloomBooks/BloomDesktop#7949) and optionally specific discussion keys to target' +--- +# Reviewable Comments + +## When To Use + +- When you look for PR comments and you notice that some comments have come in from reviewable. These will be in big unusable blocks directly on Github so you need to go to reviewable and read them individually. +- When you need to respond to comments that were created on reviewable. It is not okay to respond to them directly on GitHub. + +Use the `reviewable` CLI to read and reply to PR review discussions. + +## Environment Setup + +Two env vars must be visible to the shell running `reviewable`: + +- `REVIEWABLE_URL` — e.g. `https://reviewable.io` +- `REVIEWABLE_API_TOKEN` — looks like `rvbl_...` (from the user's personal Reviewable settings page) + +If reviewable cli is not installed, see: [https://docs.reviewable.io/agents#using-the-cli](https://docs.reviewable.io/agents#using-the-cli) + +If the user thinks that the environmental variables are there but you don't see them, ask them to restart whatever application you are running in so that the terminal environment gets the latest environment variables. + +Every command requires exactly one of: + +- `--pr=owner/repo/123` (preferred — use the GitHub PR number) +- `--branch=owner/repo/branch-name` + +Find the PR number with: `gh pr view --json number` + +## Full Workflow + +### 1. Check state + +```bash +reviewable review state --pr=BloomBooks/BloomDesktop/7949 +``` + +Check `discussions.unreplied` and `drafts.comments` to understand what needs attention. + +### 2. List all discussions + +```bash +reviewable review discussions list --pr=BloomBooks/BloomDesktop/7949 +``` + +Returns a JSON array of discussion keys. To limit to ones needing your attention: + +```bash +reviewable review discussions list --pr=BloomBooks/BloomDesktop/7949 --query="+needs:me" +``` + +### 3. View each discussion + +```bash +reviewable review discussions view --pr=BloomBooks/BloomDesktop/7949 --key="-OvkCqVS01iSEUE9u24g" +``` + +The output contains: + +- `comments`: the thread history (read these to understand what was said) +- `location.file.path` and `location.line`: which code line the discussion is on +- `location.revision.key`: which revision (r1, r2, r3...) the comment was made against +- `status.replied` / `status.resolved`: current state +- `participants[].disposition`: `blocking`, `satisfied`, `discussing`, etc. + +**Discussion key types:** + +- `-O...` keys: Reviewable-native inline comments — NOT mirrored to GitHub +- `gh-...` keys: GitHub PR review comments mirrored into Reviewable +- `-top`: the PR-level discussion (aggregates all non-inline comments — bots, general PR comments) + +### 4. Understand what was done + +For `gh-` discussions, the thread often already contains replies from the developer visible in the `comments` array with `"provenance": "github"`. Check `status.resolved` — if `true`, just acknowledge. + +For `-O` (Reviewable-native) discussions, there are usually no GitHub replies — the work was done in code changes. Read the current code at the discussion's file/line to understand what changed, then craft a reply that explains it. + +For `-top`, look at issue-level PR comments via: + +```bash +gh api repos/BloomBooks/BloomDesktop/issues/7949/comments --paginate +``` + +### 5. Reply to each discussion + +```bash +echo '{"markdownBody": "Reply text here.", "disposition": "satisfied"}' \ + | reviewable review discussions reply \ + --pr=BloomBooks/BloomDesktop/7949 \ + --key="-OvkCqVS01iSEUE9u24g" +``` + +**Required fields:** + +- `markdownBody`: non-empty Markdown reply text + +**Optional fields:** + +- `disposition`: `satisfied` | `discussing` | `blocking` | `working` + - Use `satisfied` when the concern is fully addressed + - Use `discussing` when partially addressed or when flagging a follow-up + +This creates a **draft** — not published yet. + +### 6. Acknowledge instead of replying (when no reply is needed) + +For bot-only discussions or discussions that are clearly already resolved: + +```bash +reviewable review discussions acknowledge \ + --pr=BloomBooks/BloomDesktop/7949 \ + --key="-top" \ + --disposition=satisfied +``` + +### 7. Publish all drafts + +```bash +reviewable review publish --pr=BloomBooks/BloomDesktop/7949 +``` + +Publishes all pending drafts at once. Verify with `reviewable review state` afterward (`drafts.comments` should be 0). + +To cancel a queued on-push publication: + +```bash +reviewable review publish cancel --pr=BloomBooks/BloomDesktop/7949 +``` + +## Reply authorship rule + +All replies post under the user's account. Per project policy, **prefix every reply body with `[Claude [[ORCA_RAW_HTML_INLINE:%3Cmodel%3E]]]`** (e.g. `[Claude Sonnet 4.6]`) so it is clear the text came from the AI, not the user directly. + +## Parallel fetching + +When viewing many discussions, fetch them all in parallel — issue multiple Bash tool calls in one message. Each call must re-export the env vars. + +## What "resolved" means in Reviewable + +After posting replies, discussions show `"resolved": false` until the original reviewer changes their disposition to `satisfied`. That is expected — our job is to reply with `satisfied` disposition; the reviewer decides whether to close the thread. + +## Handling the three discussion types together + +When addressing a PR with many open discussions: + +1. **Bot discussions (`gh-...`)**: often already have replies from the developer. Check `status.resolved` — if `true`, just acknowledge. If `false` and `status.replied: true`, verify the reply is substantive before skipping. +2. **Reviewer discussions (`-O...`)**: Reviewable-native, no GitHub mirror. Read the current code at the discussion location, understand what was done, and write a concrete reply. +3. **Top-level (`-top`)**: summarize how non-inline review feedback (bots, general comments) was handled. A single acknowledgement or summary reply is usually sufficient. + +## What to say + +- For concerns that were addressed in code: describe the specific change made (function name, file, what it does differently now). +- For concerns not acted on: give a concrete technical explanation of why not, not just "left as-is." +- Never be dismissive. Every reply should leave the reviewer informed. +- Use `discussing` disposition for partial fixes or acknowledged follow-up items; use `satisfied` only when fully addressed. + +## Efficiency tips + +- Fetch all discussion views in parallel (one bash call per key, all in the same message). +- Sort the work: bot discussions first (quick acks), then reviewer discussions (require code reading), then `-top` last. +- Use `--query="+needs:me"` on `list` to skip already-handled discussions when bot threads are resolved. +- The `status.replied` field tells you if you've already drafted a reply this session; `status.resolved` tells you if the original poster is satisfied. +- You can process 30+ discussions in one session by batching view calls 6-8 at a time. + diff --git a/.github/skills/reviewable-thread-replies/SKILL.md b/.github/skills/reviewable-thread-replies/SKILL.md deleted file mode 100644 index 433b6946e630..000000000000 --- a/.github/skills/reviewable-thread-replies/SKILL.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -name: reviewable-thread-replies -description: 'Reply to GitHub and Reviewable PR discussion threads one-by-one. Use whenever the user asks you to respond to review comments with accurate in-thread replies and verification.' -argument-hint: 'Repo/PR and target comments to reply to (for example: BloomBooks/BloomDesktop#7557 + specific discussion links/IDs)' -note: it's not clear that this skill is adequately developed, it's not clear that it works. ---- - -# Reviewable Thread Replies - -## What This Skill Does -Checks whether PR discussion threads still need attention and, only when they do, posts in-thread replies on both: -- GitHub PR review comments (`discussion_r...`) -- Reviewable-only discussion anchors quoted in review bodies - -## When To Use -- The user asks you to respond to one or more PR comments. -- Some comments are directly replyable on GitHub, while others only exist as Reviewable anchors. -- You need one response per thread, posted in the right place. -- You have first confirmed that the thread does not already have a verified reply with no newer reviewer follow-up. - -## Inputs -- figure out the PR using the gh cli -- Target links or IDs (GitHub `discussion_r...` or Reviewable `#-...` anchors), or enough context to discover them. -- Reply text supplied by user, or instruction to compose replies from thread context. -- If working from a markdown tracking file, current checkbox/reply status for each target. - -## Finding Threads Efficiently -- Prefer DOM-based discovery over screenshots or image-style inspection. -- Use text from the review comment, file path, line number, and nearby thread controls to locate the live thread in the DOM. -- Prefer page locators, `querySelector`, and `innerText`/text matching to find the right discussion and its active composer. -- Use page snapshots or screenshots only for coarse orientation when the DOM path is temporarily unclear. - -## Known Reviewable DOM -- These are observed DOM patterns, not stable public APIs. Reuse them when they still match, but fall back to comment-text scoping if Reviewable changes. -- Thread container: discussion content is commonly under `.discussion.details`. -- Thread working scope: for posting, the useful scope is often the parent just above `.discussion.details`, because that scope also contains the reply launcher and draft composer. -- Reply launcher: thread-local inputs commonly use `input.response-input` with placeholder `Reply…` or `Follow up…`. -- Open draft composer: active draft blocks commonly include `.relative.discussion.bottom` and `.ui.draft.comments.form`. -- Draft textarea: the editable reply body has been observed as `textarea.draft.display.textarea.mp-sensitive.sawWritingArea`. -- Send control: the post action has been observed as `.ui.basic.large.icon.send.button.item`. -- Nearby non-post controls: status buttons can appear very close to the launcher or composer, including `DONE`, `RETRACT`, `ACKNOWLEDGE`, and `RESOLVE`. -- Thread discovery pattern: find the reviewer comment text first, then scope DOM queries inside that thread instead of searching globally for launchers or textareas. -- Virtualization warning: off-screen discussions may be detached or recycled, so old handles can become stale after scrolling or reload. - -## Required Reply Format -- If the user supplies exact reply text, post that exact text. -- Otherwise, begin the composed reply with `[]`. -- Do not prepend workflow labels (for example `Will do, TODO`). -- Do not use dismissive framing such as `left as-is`, `not worth churn`, `I wouldn't bother`, or similar language that downplays a reviewer's concern. It is very good to evaluate whether we want to make a change or not, but always get the user's OK before deciding not to make a code change, but if you do end up skipping a change, explain the reasoning clearly and respectfully in the reply. -- If no code change is made, reply with a concrete explanation of the current behavior, the reasoning, and any follow-up you did instead. - -## Procedure -1. Collect and normalize targets. -- Build a list of target threads with: `target`, `context`, `response`. -- If response text is not provided, defer composing it until after you confirm the thread still needs a reply. -- Separate items into: - - GitHub direct thread comments (have comment IDs / `discussion_r...`). - - Reviewable-only threads (anchor IDs like `-Oko...`). - -2. Determine whether each target still needs attention. -- For GitHub direct thread comments, inspect the existing thread replies before drafting anything new. -- For Reviewable-only threads, inspect the visible thread history in the DOM before drafting anything new. -- If a verified reply from us already exists and there is no newer follow-up from the original commenter or another participant asking for more action, mark the target `already handled` and skip it. -- If a markdown tracking file already marks the item and its `reply:` line as completed, treat that as a strong signal that the thread may already be handled and verify against the live thread before doing more work. -- If the tracking file says `No further comment needed`, or equivalent, verify that the thread already has the expected reply and no newer follow-up; if so, skip it. -- Only compose or post a new reply when there is no verified existing reply, or when a newer reviewer comment arrived after the last verified reply. - -3. Post direct GitHub thread replies first. -- Use GitHub PR review comment reply API/tool for each direct comment ID. -- Post exactly one response per thread. -- Verify the new reply IDs/URLs are returned. - -4. Open Reviewable and navigate to the PR/thread. -- Wait for Reviewable permissions/loading state to settle before concluding that replying is blocked. -- Check whether you are already signed in before assuming auth is the problem. -- If Reviewable is not signed in, click `Sign in`. -- Use the askQuestions tool to get the user's attention and wait for them to confirm they have completed sign-in. -- After the user confirms sign-in, reload or re-check the thread and confirm the reply controls appear before posting. -- When locating the target thread, prefer DOM text search and scoped locators over visual inspection. - -5. Reply to Reviewable-only threads one by one. -- For each discussion anchor: - - Navigate to the anchor. - - Expand/open the target file or discussion until the inline thread is rendered. - - Check the existing visible thread history before opening a reply composer. - - Prefer this fast path when using Playwright locators: find the reviewer comment text, climb to the nearest `.discussion.details`, then use its parent scope for launcher/composer queries. - - Find the small thread reply launcher for that discussion. In current Reviewable UI this may be `Reply…` or `Follow up…`. - - After clicking the launcher, wait for the draft composer to replace it; the textarea may not appear synchronously. - - Type into the launcher to open the draft composer. - - If the draft composer is already open, skip the launcher and reuse the visible draft textarea instead of trying to reopen it. - - Enter the actual reply body into the draft textarea that appears below. Do not assume typing into `Follow up…` posts the reply. - - After filling the draft textarea, wait for the send arrow control to become enabled before clicking it. - - Submit the draft using the send arrow control. - - Post the user-supplied text exactly, or if composing the reply yourself, add the required `[]` prefix. - - Avoid adding status macros or extra prefixes. - - Never use nearby status controls like `DONE`, `RETRACT`, `ACKNOWLEDGE`, or `RESOLVE` as a substitute for posting the reply. -- Wait for each post to render before moving to the next thread. - -6. Verification pass. -- Re-check every target thread and confirm the expected response appears. -- Distinguish a saved draft from a posted reply: `Draft` / `draft saved` / a visible editor is not sufficient. -- Reload the page and confirm the reply still appears in the thread after the fresh render. -- Confirm no target remains unreplied due to navigation/context loss. -- Confirm no accidental text prefixes were added. -- Confirm no duplicate reply was posted to a thread that was already handled. -- If you are working from a markdown tracking file, convert the completed item line into a checked checkbox only after the reload verification succeeds. If there is a "reply:" line, make sure to also make that into a checkbox and check it so that the user knows for sure that you posted the reply successfully. - -## Decision Points -- If a target already has a verified reply from us and no newer reviewer follow-up, skip it and report `already handled` instead of drafting a new reply. -- If a tracking markdown file marks the item as replied or says no further comment is needed, verify that against the live thread before doing anything else. -- If the tracking markdown and the live thread disagree, use the live thread as the source of truth and explain the mismatch. -- If target has GitHub comment ID: use GitHub API/tool reply path. -- If target exists only in Reviewable anchor: use browser automation path. -- If Reviewable initially shows `Checking permissions` or a temporary signed-out header state: wait for the page to settle and open the target thread before deciding auth is required. -- If Reviewable is not signed in, click `Sign in`, use askQuestions to wait for the user to finish auth, then retry. -- If the inline thread never shows `Reply…`, `Follow up…`, or an already-open draft composer after that wait: authenticate first, then retry. -- If multiple visually identical reply launchers exist, use DOM scoping from the target comment text instead of image-based picking. -- A reliable Playwright pattern is: locate the comment by text, derive the thread scope from the nearest `.discussion.details` ancestor, then query `input.response-input`, `.ui.draft.comments.form`, `textarea.draft.display.textarea.mp-sensitive.sawWritingArea`, and `.ui.basic.large.icon.send.button.item` inside that scope. -- Never click `resolve`, `done`, or `acknowledge` controls and never change discussion resolution state. -- If reply input transitions into a draft composer panel: - - Treat the draft composer as the real editor and the `Reply…` / `Follow up…` input as only the launcher. - - Submit without modifying response text semantics. - - If you are composing the reply, keep the required `[]` prefix. If the user gave exact text, preserve it exactly. Avoid workflow labels. -- If Reviewable virtualizes the thread list and your earlier input handle disappears, re-find the thread by its comment text and continue from the live on-screen composer instead of relying on stale selectors. -- If posted text does not match intended response: correct immediately before continuing. - -## Quality Criteria -- Exactly one intended response posted per target thread. -- No new reply is posted to a thread that was already handled and had no newer reviewer follow-up. -- Responses are correct for thread context and preserve exact user text when supplied; otherwise they begin with `[]`. -- No unwanted prefixes like `Will do, TODO`. -- No unresolved posting errors left undocumented. -- Tracking markdown, if used, is updated only after a verified successful post. -- Final status includes: posted targets and skipped/failed targets. - -## Guardrails -- Do not post broad summary comments when thread-level replies were requested. -- Do not draft or post a fresh reply just because a comment appears in a review summary; first verify that the thread is still awaiting a response. -- Do not resolve, acknowledge, dismiss, or otherwise change PR discussion status; leave resolution actions to humans. -- Do not rely on internal/private page APIs for mutation unless officially supported and permission-safe. -- Do not assume draft state implies publication; verify thread-visible posted output. -- Do not continue after repeated auth/permission failures without reporting the blocker. -- Do not post dismissive or hand-wavy review replies; every reply should either describe the concrete code change made or give a specific technical explanation of the verified current behavior. You, the AI agent, are welcome to suggestion doing nothing to the user you are chatting with, but remember that we are not in a hurry, we are not lazy, we are not dismissive of reviewer concerns. - -## Quick Command Hints -- List PR review comments: -```bash - gh api repos///pulls//comments --paginate -``` - -- List PR reviews (to inspect review-body quoted discussions): -```bash - gh api repos///pulls//reviews --paginate -``` - From 4d97a9107b5965f2dee456a720969c5bcb21a26d Mon Sep 17 00:00:00 2001 From: John Hatton Date: Mon, 29 Jun 2026 13:55:13 -0600 Subject: [PATCH 4/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/skills/devin-review/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/skills/devin-review/SKILL.md b/.github/skills/devin-review/SKILL.md index 9492bf4cc140..1702c91c36e5 100644 --- a/.github/skills/devin-review/SKILL.md +++ b/.github/skills/devin-review/SKILL.md @@ -102,7 +102,7 @@ chrome-devtools evaluate_script "() => { const b=[...document.querySelectorAll(' - Headers like `"1 Bug"`, `"6 Flags"` with **no** `Outdated` prefix → review is current for the selected commit. Proceed to step 3. -- Headers like `"Outdated 1 Bug"`, `"Outdated6 Flags"` → these are the *previous* commit's +- Headers like "Outdated 1 Bug", "Outdated 6 Flags" → these are the *previous* commit's results; the re-review is still running. Report "Devin re-review still in progress" and come back in 5–10 minutes. Re-checking requires a reload (`chrome-devtools navigate_page --type reload`). - No Bug/Flags buttons at all → review hasn't produced results yet; come back later. From 2ec0d2b8fae33eea64531f7db001b697abb0bc8c Mon Sep 17 00:00:00 2001 From: John Hatton Date: Mon, 29 Jun 2026 13:57:30 -0600 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/skills/reviewable-comments/SKILL.md | 2 +- scripts/sync-move.mjs | 2 +- scripts/sync-pr-status.mjs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/skills/reviewable-comments/SKILL.md b/.github/skills/reviewable-comments/SKILL.md index 3ab60db31ed5..57ccb19a0ed7 100644 --- a/.github/skills/reviewable-comments/SKILL.md +++ b/.github/skills/reviewable-comments/SKILL.md @@ -1,5 +1,5 @@ --- -name: reviewable-thread-replies +name: reviewable-comments description: 'Reply to Reviewable PR discussion threads using the reviewable CLI. Use whenever the user asks you to respond to review comments.' argument-hint: 'Repo/PR reference (e.g. BloomBooks/BloomDesktop#7949) and optionally specific discussion keys to target' --- diff --git a/scripts/sync-move.mjs b/scripts/sync-move.mjs index e3b70a10ce0a..c1c30176b385 100644 --- a/scripts/sync-move.mjs +++ b/scripts/sync-move.mjs @@ -74,7 +74,7 @@ function validateStatus(status) { async function moveOrca(worktreePath, status) { const s = ORCA_STATUSES[status]; run( - `orca worktree set --worktree path:${worktreePath} --workspace-status ${s} --json`, + `orca worktree set --worktree "path:${worktreePath}" --workspace-status ${s} --json`, ); console.log(`✓ Orca: ${worktreePath} → ${s}`); } diff --git a/scripts/sync-pr-status.mjs b/scripts/sync-pr-status.mjs index 56f248fa57e7..a4c4d6f2d0d3 100644 --- a/scripts/sync-pr-status.mjs +++ b/scripts/sync-pr-status.mjs @@ -52,7 +52,7 @@ function extractBl(text) { async function setOrcaStatus(worktreePath, statusKey) { run( - `orca worktree set --worktree path:${worktreePath} --workspace-status ${ORCA_STATUSES[statusKey]}`, + `orca worktree set --worktree "path:${worktreePath}" --workspace-status ${ORCA_STATUSES[statusKey]}`, ); } @@ -98,7 +98,7 @@ async function main() { // 2. GitHub board status by PR number (single batch call) const boardRaw = run( - `gh project item-list ${GH_PROJECT_NUMBER} --owner ${GH_OWNER} --format json --limit 200`, + `gh project item-list ${GH_PROJECT_NUMBER} --owner ${GH_OWNER} --format json --limit 500`, ); const boardData = JSON.parse(boardRaw); const boardByPr = {}; From c50eacb072cd27db662d52ec54706fc2fd5a6e95 Mon Sep 17 00:00:00 2001 From: Hatton Date: Mon, 29 Jun 2026 14:38:01 -0600 Subject: [PATCH 6/7] sync-pr-status: reconcile YouTrack even when Orca status already matches The reconciler skipped a worktree entirely when its Orca status already matched the GitHub board, treating Orca status as a proxy for full sync. A YouTrack update that failed transiently (network/token) on an earlier cycle was therefore never retried, leaving the tracker permanently wrong. Now we skip only the Orca write when it already matches and still fall through to reconcile YouTrack. To avoid re-issuing the same State command every cycle, setYtState first reads the current state (new getYtState) and no-ops when already correct. This also self-heals manual 'orca worktree set' or partial sync-move runs. --- scripts/sync-pr-status.mjs | 60 ++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/scripts/sync-pr-status.mjs b/scripts/sync-pr-status.mjs index a4c4d6f2d0d3..1120bad9a408 100644 --- a/scripts/sync-pr-status.mjs +++ b/scripts/sync-pr-status.mjs @@ -56,6 +56,25 @@ async function setOrcaStatus(worktreePath, statusKey) { ); } +/** Read an issue's current State field value (name), or null if unset/unknown. */ +async function getYtState(issueId) { + const res = await fetch( + `${YT_BASE}/api/issues/${issueId}?fields=customFields(name,value(name))`, + { + headers: { + Authorization: `Bearer ${YT_TOKEN}`, + Accept: "application/json", + }, + }, + ); + if (!res.ok) { + throw new Error(`YouTrack GET ${res.status}: ${await res.text()}`); + } + const data = await res.json(); + const stateField = data.customFields?.find((f) => f.name === "State"); + return stateField?.value?.name ?? null; +} + async function setYtState(issueId, statusKey) { const state = YT_STATES[statusKey]; if (!state) return false; @@ -64,6 +83,12 @@ async function setYtState(issueId, statusKey) { return false; } + // Idempotent: only issue the command if the issue isn't already in the + // target state. This lets a later reconciliation cycle self-heal a + // YouTrack update that failed transiently on an earlier cycle, without + // re-applying (and re-logging activity for) state that's already correct. + if ((await getYtState(issueId)) === state) return false; + const res = await fetch(`${YT_BASE}/api/commands`, { method: "POST", headers: { @@ -147,12 +172,11 @@ async function main() { } const targetOrcaStatus = ORCA_STATUSES[targetKey]; - if (currentOrcaStatus === targetOrcaStatus) { - console.log(` ${label}: already ${currentOrcaStatus} ✓`); - continue; - } + const orcaInSync = currentOrcaStatus === targetOrcaStatus; - // Resolve BL# for YouTrack: branch → display name → PR title/body + // Resolve BL# for YouTrack: branch → display name → PR title/body. + // We resolve it even when Orca is already in sync, because YouTrack is + // reconciled independently below (see "Apply YouTrack update"). let ytIssue = extractBl(wt.git?.branch) ?? extractBl(wt.displayName); if (!ytIssue) { try { @@ -166,16 +190,22 @@ async function main() { } } - // Apply Orca update - try { - await setOrcaStatus(wt.path, targetKey); - console.log( - `✓ ${label}: Orca ${currentOrcaStatus} → ${targetOrcaStatus}`, - ); - changed++; - } catch (e) { - console.error(` ${label}: Orca update failed: ${e.message}`); - continue; + // Apply Orca update (skip the write when it already matches, but still + // fall through to reconcile YouTrack — a matching Orca status does NOT + // imply YouTrack succeeded on a previous cycle). + if (orcaInSync) { + console.log(` ${label}: Orca already ${currentOrcaStatus} ✓`); + } else { + try { + await setOrcaStatus(wt.path, targetKey); + console.log( + `✓ ${label}: Orca ${currentOrcaStatus} → ${targetOrcaStatus}`, + ); + changed++; + } catch (e) { + console.error(` ${label}: Orca update failed: ${e.message}`); + continue; + } } // Apply YouTrack update From 15743d8ff00a74392216cdf1c53d2ae7973a8065 Mon Sep 17 00:00:00 2001 From: Hatton Date: Mon, 29 Jun 2026 15:05:00 -0600 Subject: [PATCH 7/7] Address PR review: shared sync constants, sync-move limit + YouTrack guard - Extract duplicated status mappings (ORCA_STATUSES, YT_STATES, GH_OWNER, GH_PROJECT_NUMBER, YT_BASE) into scripts/sync-constants.mjs, imported by both sync-move.mjs and sync-pr-status.mjs so they can no longer drift. - sync-move.mjs: bump 'gh project item-list' --limit from 200 to 500 to match sync-pr-status.mjs and avoid silently truncating the board. - sync-move.mjs: wrap the YouTrack call in moveAll with try/catch so a YouTrack failure (e.g. missing token) no longer hard-exits after GitHub and Orca were already updated, matching the warn-and-skip contract. --- scripts/sync-constants.mjs | 29 +++++++++++++++++++++++++++ scripts/sync-move.mjs | 41 +++++++++++++++++--------------------- scripts/sync-pr-status.mjs | 31 +++++++++------------------- 3 files changed, 56 insertions(+), 45 deletions(-) create mode 100644 scripts/sync-constants.mjs diff --git a/scripts/sync-constants.mjs b/scripts/sync-constants.mjs new file mode 100644 index 000000000000..1c31abe1f2ac --- /dev/null +++ b/scripts/sync-constants.mjs @@ -0,0 +1,29 @@ +/** + * sync-constants.mjs — shared status mappings for the PR-sync scripts. + * + * Imported by both sync-move.mjs and sync-pr-status.mjs so the two can never + * drift out of sync (e.g. a status rename fixed in one file but not the other). + * Zero AI tokens; all mappings are hardcoded. + */ + +// ── GitHub project constants ────────────────────────────────────────────────── +export const GH_OWNER = "BloomBooks"; +export const GH_PROJECT_NUMBER = 2; + +// ── Internal key → Orca workspaceStatus value ───────────────────────────────── +export const ORCA_STATUSES = { + "waiting-ai": "status-5", + "in-review": "in-review", + "has-comments": "status-6", + completed: "completed", +}; + +// ── Internal key → YouTrack state name (null = leave for human judgment) ─────── +export const YT_STATES = { + "waiting-ai": "In Progress", + "in-review": "Ready For Code Review", + "has-comments": "Has Comments", + completed: null, // type-dependent: bug→Closed, feature→Ready For Testing +}; + +export const YT_BASE = "https://issues.bloomlibrary.org/youtrack"; diff --git a/scripts/sync-move.mjs b/scripts/sync-move.mjs index c1c30176b385..8cafc847e0fc 100644 --- a/scripts/sync-move.mjs +++ b/scripts/sync-move.mjs @@ -20,10 +20,15 @@ */ import { execSync } from "child_process"; - -// ── GitHub project constants ────────────────────────────────────────────────── -const GH_OWNER = "BloomBooks"; -const GH_PROJECT_NUMBER = 2; +import { + GH_OWNER, + GH_PROJECT_NUMBER, + ORCA_STATUSES, + YT_STATES, + YT_BASE, +} from "./sync-constants.mjs"; + +// ── GitHub project constants (move-only; the board edit needs these IDs) ────── const GH_PROJECT_ID = "PVT_kwDOAFlSFM4Bawkp"; const GH_STATUS_FIELD = "PVTSSF_lADOAFlSFM4BawkpzhVl0_w"; @@ -35,23 +40,6 @@ const GH_OPTIONS = { completed: null, // auto-hidden when PR closes }; -// YouTrack state names (exact strings from the bundle) -const YT_STATES = { - "waiting-ai": "In Progress", - "in-review": "Ready For Code Review", - "has-comments": "Has Comments", - completed: null, // leave for human (type-dependent: bug→Closed, feature→Ready For Testing) -}; - -// Orca workspace-status IDs -const ORCA_STATUSES = { - "waiting-ai": "status-5", - "in-review": "in-review", - "has-comments": "status-6", - completed: "completed", -}; - -const YT_BASE = "https://issues.bloomlibrary.org/youtrack"; const YT_TOKEN = process.env.YOUTRACK; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -90,7 +78,7 @@ async function moveGh(prNumber, status) { // Look up the board item ID for this PR number const raw = run( - `gh project item-list ${GH_PROJECT_NUMBER} --owner ${GH_OWNER} --format json --limit 200`, + `gh project item-list ${GH_PROJECT_NUMBER} --owner ${GH_OWNER} --format json --limit 500`, ); const data = JSON.parse(raw); const item = data.items.find((i) => i.content?.number === Number(prNumber)); @@ -177,7 +165,14 @@ async function moveAll(prNumber, status) { } if (ytIssue) { - await moveYt(ytIssue, status); + // GitHub and Orca were already mutated above, so a YouTrack failure + // here must not abort the whole command. Warn and skip to match + // sync-pr-status.mjs's warn-and-skip contract. + try { + await moveYt(ytIssue, status); + } catch (e) { + console.warn(` YT: ${e.message} — skipping YouTrack`); + } } else { console.warn( ` YT: no BL-xxxxx found for PR #${prNumber} — skipping YouTrack`, diff --git a/scripts/sync-pr-status.mjs b/scripts/sync-pr-status.mjs index 1120bad9a408..a1ca125bed6a 100644 --- a/scripts/sync-pr-status.mjs +++ b/scripts/sync-pr-status.mjs @@ -10,34 +10,21 @@ */ import { execSync } from "child_process"; - -const GH_OWNER = "BloomBooks"; -const GH_PROJECT_NUMBER = 2; - -// GitHub board status label → internal key +import { + GH_OWNER, + GH_PROJECT_NUMBER, + ORCA_STATUSES, + YT_STATES, + YT_BASE, +} from "./sync-constants.mjs"; + +// GitHub board status label → internal key (reconciler-only) const GH_STATUS_TO_KEY = { "Waiting for AI-Review": "waiting-ai", "Ready for Human": "in-review", "Has Comments": "has-comments", }; -// Internal key → Orca workspaceStatus value -const ORCA_STATUSES = { - "waiting-ai": "status-5", - "in-review": "in-review", - "has-comments": "status-6", - completed: "completed", -}; - -// Internal key → YouTrack state name (null = leave for human) -const YT_STATES = { - "waiting-ai": "In Progress", - "in-review": "Ready For Code Review", - "has-comments": "Has Comments", - completed: null, -}; - -const YT_BASE = "https://issues.bloomlibrary.org/youtrack"; const YT_TOKEN = process.env.YOUTRACK; function run(cmd) {