From ed8c9af7643a3dd24375a974fb2314f1863c151e Mon Sep 17 00:00:00 2001 From: wicolian Date: Tue, 25 Aug 2026 19:23:01 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20PRD=20policy=20=E2=80=94=20templates,?= =?UTF-8?q?=20gate,=20and=20org=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every feature and hotfix gets a PRD, approved before the PR is reviewed. Agreed by Rahul and Koushik, August 2026. Today nothing enforces this. I checked: 10 PRD-ish issues across frontend-mono and backend, none labelled, no issue template in any repo, and PRDs scattered across issue bodies, repo .md files, Google Docs and Slack threads. Two Slack reminders have not fixed it, because a rule people must remember is not a gate. What this adds: - Two org-default issue forms. PRD (full) for anything crossing a repo boundary, changing a contract, or needing a migration. PRD (lite), one page, for fixes and hotfix RCAs. Rahul asked for a shorter template; a single long one would get routed around. - A reusable PRD gate workflow. A PR merges only if it references an issue carrying both PRD and prd-approved, or if a maintainer applies prd-exempt. Reads the PR body, title and branch name for #123, owner/repo#123 and full issue URLs. Cross-repo lookups need the PRD_GATE_TOKEN secret. - A PR template whose second section is "Deviations from the PRD". - docs/PRD-POLICY.md and docs/PRD-TEMPLATE.md. The template is the skeleton both existing PRDs already use, written down for the first time. - setup/install-prd-gate.sh creates the labels and can open the caller PRs. It never deletes and never force-pushes. Nothing enforces until someone makes "PRD gate / prd" a required status check. Until then the gate reports and does not block. Claude-Session: https://claude.ai/code/session_01E2afbz3AS1tATwMc5iaNCu --- .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/prd-lite.yml | 66 ++++++++++ .github/ISSUE_TEMPLATE/prd.yml | 112 +++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 23 ++++ .github/workflows/prd-gate.yml | 181 ++++++++++++++++++++++++++++ docs/PRD-POLICY.md | 98 +++++++++++++++ docs/PRD-TEMPLATE.md | 53 ++++++++ setup/caller-workflow.yml | 24 ++++ setup/install-prd-gate.sh | 68 +++++++++++ 9 files changed, 630 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/prd-lite.yml create mode 100644 .github/ISSUE_TEMPLATE/prd.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/prd-gate.yml create mode 100644 docs/PRD-POLICY.md create mode 100644 docs/PRD-TEMPLATE.md create mode 100644 setup/caller-workflow.yml create mode 100755 setup/install-prd-gate.sh diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0e44741 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: PRD policy — read this first + url: https://github.com/databrainhq/.github/blob/main/docs/PRD-POLICY.md + about: Why every feature and hotfix needs a PRD, and which template to pick. diff --git a/.github/ISSUE_TEMPLATE/prd-lite.yml b/.github/ISSUE_TEMPLATE/prd-lite.yml new file mode 100644 index 0000000..69e971b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/prd-lite.yml @@ -0,0 +1,66 @@ +name: "PRD (lite)" +description: "Bug fix, hotfix RCA, or a small self-contained feature. One page." +title: "PRD: " +labels: ["PRD", "prd:lite", "prd:draft"] +body: + - type: markdown + attributes: + value: | + One-page PRD. Use this for fixes, hotfix RCAs, and small self-contained + features inside one repo. + + If the change crosses a repo boundary, changes a contract, or needs a + migration, use **PRD (full)** instead. + + A PR cannot merge until this issue carries the `prd-approved` label. + + - type: input + id: owner + attributes: + label: Owner + validations: { required: true } + + - type: dropdown + id: repo + attributes: + label: Repo + options: [frontend-mono, backend, plugin-in-app, mcp-server, hasura, docs, website, other] + validations: { required: true } + + - type: dropdown + id: kind + attributes: + label: Kind + options: ["feature (small)", "bug fix", "hotfix / RCA", "chore with user-visible effect"] + validations: { required: true } + + - type: textarea + id: shipping + attributes: + label: What are we shipping? + description: > + For a hotfix, this is the RCA. What broke, why it broke, and what stops it + happening again. + validations: { required: true } + + - type: textarea + id: success + attributes: + label: How do we know it worked? + description: What you check after the merge. + validations: { required: true } + + - type: textarea + id: notdoing + attributes: + label: What are we explicitly NOT doing? + description: Keeps the fix from growing in review. One or two lines is fine. + validations: { required: true } + + - type: checkboxes + id: ack + attributes: + label: Before you request review + options: + - label: "This really is small enough for a lite PRD." + required: true diff --git a/.github/ISSUE_TEMPLATE/prd.yml b/.github/ISSUE_TEMPLATE/prd.yml new file mode 100644 index 0000000..08fc011 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/prd.yml @@ -0,0 +1,112 @@ +name: "PRD (full)" +description: "Feature or change that crosses a repo boundary, changes a contract, or needs a migration." +title: "PRD: " +labels: ["PRD", "prd:draft"] +body: + - type: markdown + attributes: + value: | + Full PRD. Use this when the change crosses a repo boundary, changes an API or + storage contract, needs a migration, or a reviewer could reasonably disagree + about the approach. + + Smaller than that? Close this and open a **PRD (lite)** instead. + + A PR cannot merge until this issue carries the `prd-approved` label. + + - type: input + id: owner + attributes: + label: Owner + description: One name. The person who answers review comments. + validations: { required: true } + + - type: dropdown + id: repos + attributes: + label: Repos affected + multiple: true + options: [frontend-mono, backend, plugin-in-app, mcp-server, hasura, docs, website, other] + validations: { required: true } + + - type: textarea + id: links + attributes: + label: Links + description: Specs, ADRs, superseded docs, related PRDs. One per line. + placeholder: | + Schema: specs/semantic-layer-v2.md + Decision: docs/adr/0001-....md + Supersedes: #1234 + + - type: textarea + id: shipping + attributes: + label: What are we shipping? + description: The thing itself, not the motivation. One paragraph. + validations: { required: true } + + - type: textarea + id: success + attributes: + label: How do we know it worked? + description: Outcomes you can check after the merge. Not a feeling. + validations: { required: true } + + - type: textarea + id: hard + attributes: + label: What's the hard part? + description: The real constraint. If there is no hard part, use PRD (lite) instead. + validations: { required: true } + + - type: textarea + id: notdoing + attributes: + label: What are we explicitly NOT doing? + description: > + The most important section. List what is cut. Mark each as cut or deferred and + mean it. Anything revived later needs its own PRD. + validations: { required: true } + + - type: textarea + id: scope + attributes: + label: Scope + description: What ships together, and what is out. + value: | + | | In | Out | + | ------------------- | --- | --- | + | Ships together | | | + | Nice later, no date | | | + validations: { required: true } + + - type: input + id: contract + attributes: + label: One contract + description: The single sentence a reviewer should be able to repeat back. + validations: { required: true } + + - type: textarea + id: rollout + attributes: + label: Rollout + description: Flags, migrations, merge order across repos, and the back-out. + validations: { required: true } + + - type: textarea + id: risks + attributes: + label: Risks + description: What breaks if we are wrong, and who notices first. + + - type: checkboxes + id: ack + attributes: + label: Before you request review + options: + - label: "The NOT doing section is filled in, not left as a placeholder." + required: true + - label: "I have read docs/PRD-POLICY.md." + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..45f0a45 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + + +**PRD:** # + +## What changed + + + +## Deviations from the PRD + + + +None. + +## How this was verified + + diff --git a/.github/workflows/prd-gate.yml b/.github/workflows/prd-gate.yml new file mode 100644 index 0000000..455223b --- /dev/null +++ b/.github/workflows/prd-gate.yml @@ -0,0 +1,181 @@ +# Reusable PRD gate. Called by each repo that enforces the policy. +# +# A PR may merge only if it references an issue that carries BOTH the `PRD` +# and `prd-approved` labels — or if the PR itself carries `prd-exempt`. +# +# Caller usage (see setup/caller-workflow.yml): +# jobs: +# prd: +# uses: databrainhq/.github/.github/workflows/prd-gate.yml@main +# secrets: +# cross_repo_token: ${{ secrets.PRD_GATE_TOKEN }} + +name: PRD gate + +on: + workflow_call: + inputs: + exempt_label: + description: Label on the PR that skips the gate. + type: string + default: prd-exempt + approved_label: + description: Label the PRD issue must carry. + type: string + default: prd-approved + secrets: + cross_repo_token: + description: > + Token that can read issues in other databrainhq repos. Optional — without + it, only same-repo PRD references can be verified. + required: false + +permissions: + contents: read + pull-requests: read + issues: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + env: + EXEMPT_LABEL: ${{ inputs.exempt_label }} + APPROVED_LABEL: ${{ inputs.approved_label }} + CROSS_REPO_TOKEN: ${{ secrets.cross_repo_token }} + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { core.setFailed('PRD gate must run on pull_request events.'); return; } + + const EXEMPT = process.env.EXEMPT_LABEL; + const APPROVED = process.env.APPROVED_LABEL; + const POLICY = 'https://github.com/databrainhq/.github/blob/main/docs/PRD-POLICY.md'; + + // --- escape hatch ------------------------------------------------ + const prLabels = (pr.labels || []).map(l => l.name.toLowerCase()); + if (prLabels.includes(EXEMPT.toLowerCase())) { + core.notice(`PR carries "${EXEMPT}" — gate skipped. A human with write access applied it.`); + return; + } + + // --- collect candidate PRD references ---------------------------- + // Sources: PR body, PR title, and the branch name. + const haystack = [pr.body || '', pr.title || '', pr.head?.ref || ''].join('\n'); + const owner = context.repo.owner; + const refs = new Map(); // key -> {owner, repo, number} + + const add = (o, r, n) => { + const key = `${o}/${r}#${n}`; + if (!refs.has(key)) refs.set(key, { owner: o, repo: r, number: Number(n) }); + }; + + // https://github.com/OWNER/REPO/issues/123 + for (const m of haystack.matchAll(/https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/issues\/(\d+)/gi)) { + add(m[1], m[2], m[3]); + } + // OWNER/REPO#123 + for (const m of haystack.matchAll(/\b([\w.-]+)\/([\w.-]+)#(\d+)\b/g)) { + add(m[1], m[2], m[3]); + } + // bare #123 -> this repo + for (const m of haystack.matchAll(/(?:^|[^\w/])#(\d+)\b/g)) { + add(owner, context.repo.repo, m[1]); + } + + if (refs.size === 0) { + core.setFailed( + 'No PRD linked.\n\n' + + 'Add a line to the PR description that links the PRD issue, for example:\n' + + ' PRD: #1234\n' + + ' PRD: databrainhq/backend#3379\n\n' + + `The issue must carry the "PRD" and "${APPROVED}" labels.\n` + + `Policy: ${POLICY}` + ); + return; + } + + // --- resolve each reference -------------------------------------- + const crossToken = process.env.CROSS_REPO_TOKEN; + + // Cross-repo reads go over plain fetch so we never depend on a second + // Octokit instance being require-able inside github-script. + async function readIssue(ref, sameRepo) { + if (sameRepo) { + const r = await github.rest.issues.get({ + owner: ref.owner, repo: ref.repo, issue_number: ref.number, + }); + return r.data; + } + if (!crossToken) { + const e = new Error('no PRD_GATE_TOKEN secret set'); + e.noToken = true; + throw e; + } + const res = await fetch( + `https://api.github.com/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, + { headers: { + Authorization: `Bearer ${crossToken}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'databrainhq-prd-gate', + } }, + ); + if (!res.ok) { + const e = new Error(`HTTP ${res.status}`); + e.status = res.status; + throw e; + } + return res.json(); + } + + const report = []; + let approvedRef = null; + + for (const ref of refs.values()) { + const sameRepo = ref.owner === owner && ref.repo === context.repo.repo; + let issue; + try { + issue = await readIssue(ref, sameRepo); + } catch (e) { + report.push( + `- ${ref.owner}/${ref.repo}#${ref.number} — could not read (${e.status || e.message})` + + (e.noToken ? '. Set the PRD_GATE_TOKEN secret to allow cross-repo checks.' : '') + ); + continue; + } + + // A PR is not a PRD, even though the issues API returns both. + if (issue.pull_request) { + report.push(`- ${ref.owner}/${ref.repo}#${ref.number} — is a pull request, not a PRD issue`); + continue; + } + + const labels = (issue.labels || []) + .map(l => (typeof l === 'string' ? l : l.name).toLowerCase()); + const isPrd = labels.includes('prd'); + const isApproved = labels.includes(APPROVED.toLowerCase()); + + if (isPrd && isApproved) { approvedRef = { ref, issue }; break; } + if (isPrd) report.push(`- ${ref.owner}/${ref.repo}#${ref.number} "${issue.title}" — PRD found, but not yet ${APPROVED}`); + else report.push(`- ${ref.owner}/${ref.repo}#${ref.number} "${issue.title}" — not labelled PRD`); + } + + if (approvedRef) { + const { ref, issue } = approvedRef; + core.notice(`Approved PRD: ${ref.owner}/${ref.repo}#${ref.number} — ${issue.title}`); + await core.summary + .addHeading('PRD gate passed', 3) + .addRaw(`Approved PRD: [${ref.owner}/${ref.repo}#${ref.number}](${issue.html_url}) — ${issue.title}`) + .write(); + return; + } + + core.setFailed( + `No approved PRD found for this PR.\n\n${report.join('\n')}\n\n` + + `Fix one of these:\n` + + ` 1. Link the right PRD issue in the PR description ("PRD: #1234").\n` + + ` 2. Get the PRD approved — a reviewer adds the "${APPROVED}" label.\n` + + ` 3. If this genuinely needs no PRD, a maintainer adds "${EXEMPT}" to this PR.\n\n` + + `Policy: ${POLICY}` + ); diff --git a/docs/PRD-POLICY.md b/docs/PRD-POLICY.md new file mode 100644 index 0000000..cbe470f --- /dev/null +++ b/docs/PRD-POLICY.md @@ -0,0 +1,98 @@ +# PRD policy + +Every feature and every hotfix gets a PRD, approved before the PR is reviewed. + +Agreed by Rahul and Koushik, August 2026. This document is the rule; the CI gate +is the enforcement. + +--- + +## The rule + +1. **Write the PRD first.** Open an issue with the **PRD (full)** or **PRD (lite)** + template. It lands with the `PRD` and `prd:draft` labels. +2. **Get comments.** Post it in the team channel. Everyone reviews the approach, + not the code, because there is no code yet. +3. **Get approval.** Sapna signs off on architecture and codebase fit. The approver + removes `prd:draft` and adds `prd-approved`. +4. **Then raise the PR.** Link the PRD in the PR description: `PRD: #1234`. +5. **CI checks it.** A PR whose PRD is missing or unapproved cannot merge. + +We are not reviewing PRs without an approved PRD. That is the whole point: an audit +trail of what we built and why, written while the decision was still open. + +## Which template + +| Situation | Template | +| --------------------------------------------------------------- | ---------- | +| Crosses a repo boundary | full | +| Changes an API, storage, or embed contract | full | +| Needs a migration or a flag | full | +| A reviewer could reasonably disagree about the approach | full | +| Small self-contained feature in one repo | lite | +| Bug fix | lite | +| Hotfix — **the PRD is the RCA** | lite | +| Dependency bump, revert, generated file, typo | none: use `prd-exempt` | + +If a full PRD gets too long to read in one sitting, that is a signal to cut scope, +not to split it into linked documents. + +## The section that matters + +**What are we explicitly NOT doing.** + +It is the section reviewers should read first and the one most often left empty. +Mark each item **cut** or **deferred**, and mean it. Anything revived later needs +its own PRD. A PRD without this section is not approved, no matter how good the +rest is. + +## Escape hatch + +A maintainer may add `prd-exempt` to a PR to skip the gate. Say why in the PR +description. Anyone with write access can apply it, and every use is visible in +the PR timeline. It exists so the gate never blocks a real outage fix — use it for +that, then write the RCA as a lite PRD afterwards. + +## Where PRDs live + +**In a GitHub issue, in the repo the work lands in.** Not Google Docs, not a Slack +thread, not a loose `.md` on a branch. Those all still exist from before this +policy and are fine as links, but a new PRD is an issue, so the gate can read it +and so the audit trail sits next to the code. + +Cross-repo work: open the PRD in the repo where most of the change lands and link +it from the others as `databrainhq/backend#3379`. + +## Labels + +| Label | Meaning | +| -------------- | -------------------------------------------------------------- | +| `PRD` | This issue is a PRD. | +| `prd:draft` | Being written. Not ready for review. | +| `prd:lite` | One-pager. | +| `prd-approved` | Approved. PRs referencing it may merge. | +| `prd-exempt` | On a **PR**, not an issue. Skips the gate. | + +Only `prd-approved` is load-bearing for CI. The others are for humans. + +## Setup + +See [`setup/install-prd-gate.sh`](../setup/install-prd-gate.sh). Three steps cannot +be scripted safely and need an org admin: + +1. `databrainhq/.github` → Settings → Actions → General → Access → allow org repos. +2. Add the org secret `PRD_GATE_TOKEN` (read access to Issues) for cross-repo links. +3. Make `PRD gate / prd` a required status check on each repo's default branch. + +Until step 3 is done the gate reports but does not block. + +## Writing a PRD with Edith + +`/edith-prd ` drafts a PRD into an existing issue: it reads the +repo, reads the three most recently approved PRDs as style examples, and posts a +draft as a comment for you to edit. + +Edith never approves a PRD and never applies `prd-approved`. A person does that. + +Once approved, `/edith-build ` hands the PRD to a Cursor cloud agent, +which branches, implements, and opens a PR that links back to the PRD. diff --git a/docs/PRD-TEMPLATE.md b/docs/PRD-TEMPLATE.md new file mode 100644 index 0000000..17a8a32 --- /dev/null +++ b/docs/PRD-TEMPLATE.md @@ -0,0 +1,53 @@ +# PRD: + +| | | +| -------- | ---------------------------------------------------- | +| Status | draft \| ready-for-review \| ready-for-impl \| shipped | +| Owner | | +| Date | | +| Repo | | +| Links | | + +Fill this before writing code. Keep answers short. Link the filled doc from the PR. + +--- + +## Problem + +**What are we shipping?** + +One paragraph. The thing itself, not the motivation. + +**How do we know it worked?** + +Observable outcomes. Something you can check after the merge, not a feeling. + +**What's the hard part?** + +The real constraint. If there is no hard part, say so and use PRD-lite instead. + +**What are we explicitly NOT doing?** + +The most important section. List what is cut. Say **cut** or **deferred** for each, +and mean it. Anything revived later needs its own PRD. + +--- + +## Scope + +| | In | Out | +| ------------------- | --- | --- | +| Ships together | | | +| Nice later, no date | | | + +**One contract:** the single sentence a reviewer should be able to repeat back. + +--- + +## Rollout + +How it lands. Flags, migrations, order of merge across repos, and the back-out. + +## Risks + +What breaks if we are wrong, and who notices first. diff --git a/setup/caller-workflow.yml b/setup/caller-workflow.yml new file mode 100644 index 0000000..b5696b1 --- /dev/null +++ b/setup/caller-workflow.yml @@ -0,0 +1,24 @@ +# Copy this file to .github/workflows/prd-gate.yml in each repo that enforces the +# PRD policy (frontend-mono, backend, plugin-in-app). +# +# One-time prerequisites: +# 1. databrainhq/.github must allow its workflows to be reused by org repos: +# Settings -> Actions -> General -> Access -> "Accessible from repositories +# in the databrainhq organization". +# 2. Add the org secret PRD_GATE_TOKEN (a fine-grained PAT or GitHub App token +# with read access to Issues on databrainhq repos). Without it the gate can +# still verify same-repo PRD links, but not cross-repo ones. +# 3. Make "PRD gate / check" a required status check on the default branch: +# Settings -> Branches -> branch protection -> Require status checks. + +name: PRD gate + +on: + pull_request: + types: [opened, edited, reopened, synchronize, labeled, unlabeled] + +jobs: + prd: + uses: databrainhq/.github/.github/workflows/prd-gate.yml@main + secrets: + cross_repo_token: ${{ secrets.PRD_GATE_TOKEN }} diff --git a/setup/install-prd-gate.sh b/setup/install-prd-gate.sh new file mode 100755 index 0000000..53c9388 --- /dev/null +++ b/setup/install-prd-gate.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Creates the PRD labels and installs the gate caller workflow in the target repos. +# +# ./setup/install-prd-gate.sh --dry-run # show what would happen +# ./setup/install-prd-gate.sh # create labels +# ./setup/install-prd-gate.sh --with-workflow # labels + open a PR adding the caller +# +# Needs: gh CLI, authenticated with repo admin on databrainhq. +# This script never force-pushes and never deletes anything. + +set -euo pipefail + +REPOS=(frontend-mono backend plugin-in-app) +ORG=databrainhq +DRY=0 +WITH_WORKFLOW=0 + +for a in "$@"; do + case "$a" in + --dry-run) DRY=1 ;; + --with-workflow) WITH_WORKFLOW=1 ;; + *) echo "unknown flag: $a" >&2; exit 2 ;; + esac +done + +run() { if [ "$DRY" = 1 ]; then echo "DRY: $*"; else "$@"; fi; } + +# name|color|description +LABELS=( + "PRD|0E8A16|This issue is a PRD. Required before a PR can merge." + "prd:draft|FBCA04|PRD is being written. Not ready for review." + "prd:lite|C5DEF5|One-page PRD. Fix, hotfix RCA, or small feature." + "prd-approved|0052CC|PRD approved. PRs may now reference this issue and merge." + "prd-exempt|5319E7|This PR skips the PRD gate. Applied by a maintainer, with a reason in the PR." +) + +for repo in "${REPOS[@]}"; do + echo "== $ORG/$repo ==" + for spec in "${LABELS[@]}"; do + IFS='|' read -r name color desc <<< "$spec" + # --force updates colour/description if the label already exists; it never deletes. + run gh label create "$name" --repo "$ORG/$repo" --color "$color" --description "$desc" --force + done + + if [ "$WITH_WORKFLOW" = 1 ]; then + tmp="$(mktemp -d)" + run gh repo clone "$ORG/$repo" "$tmp/$repo" -- --depth=1 + if [ "$DRY" = 0 ]; then + mkdir -p "$tmp/$repo/.github/workflows" + cp "$(dirname "$0")/caller-workflow.yml" "$tmp/$repo/.github/workflows/prd-gate.yml" + git -C "$tmp/$repo" checkout -b chore/prd-gate + git -C "$tmp/$repo" add .github/workflows/prd-gate.yml + git -C "$tmp/$repo" commit -m "ci: require an approved PRD before a PR can merge" + git -C "$tmp/$repo" push -u origin chore/prd-gate + gh pr create --repo "$ORG/$repo" --base main --head chore/prd-gate \ + --title "ci: PRD gate" \ + --body "Adds the shared PRD gate. See databrainhq/.github/docs/PRD-POLICY.md. + +Not enforcing until someone makes \`PRD gate / prd\` a required status check on \`main\`." + fi + fi +done + +echo +echo "Labels done. Remaining manual steps (cannot be scripted safely):" +echo " 1. databrainhq/.github -> Settings -> Actions -> General -> Access -> allow org repos." +echo " 2. Add org secret PRD_GATE_TOKEN (read access to Issues)." +echo " 3. Make 'PRD gate / prd' a required status check on each repo's default branch."