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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/skills/issues/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ paragraph; put the smallest next action in **Detail / next action**.
open or resolved table as appropriate.
- IDs are monotonic and never reused — always allocate from the `issues:next-id` marker and bump it.
- Escape `|` inside cell text (write `\|`) so the markdown table stays intact.
- This file uses `merge=union` in `.gitattributes`. Never resolve a conflict by taking one side
wholesale — that drops the other agent's rows. `npm run check:outstanding-issues` fails on
duplicate IDs or a stale next-id marker.
- Respect the repo's RAG/clinical/privacy flagging rules if an item _itself_ touches a protected
surface — recording it here is fine, but acting on it later still needs the usual gate.

Expand Down
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@
# and drops exact duplicate dated rows (stock `union` kept concurrent appends
# but also reintroduced babysit twins that failed the ledger guard).
docs/branch-review-ledger.md merge=ledger

# Outstanding-issue rows are also append-mostly across concurrent agent sessions.
# Union merge preserves both sides' rows; scripts/check-outstanding-issues.mjs
# still fails on duplicate IDs because row-level union alone cannot allocate IDs.
docs/outstanding-issues.md merge=union
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ jobs:
- name: Outstanding-issues ledger integrity
run: npm run check:outstanding-issues

- name: PR mergeability workflow contract
run: npm run check:pr-mergeability

- name: Codebase index coverage
run: npm run docs:check-index

Expand Down
202 changes: 202 additions & 0 deletions .github/workflows/pr-mergeability.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
name: PR mergeability

on:
pull_request_target:
branches: [main, "release/**"]
types: [opened, synchronize, reopened, ready_for_review, edited]
Comment thread
BigSimmo marked this conversation as resolved.
push:
branches: [main, "release/**"]

concurrency:
group: pr-mergeability-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: read

jobs:
mergeability:
name: PR mergeability
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
# pull_request_target runs trusted workflow-revision code. Checkout
# github.workflow_sha so the classifier is the exact revision that
# triggered this run. Never execute the PR head or persist credentials.
- name: Checkout trusted classifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.workflow_sha }}
persist-credentials: false

- name: Signal real merge conflicts
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { pathToFileURL } = require("node:url");
const moduleUrl = pathToFileURL(`${process.env.GITHUB_WORKSPACE}/scripts/pr-mergeability.mjs`).href;
const { classifyMergeability } = await import(moduleUrl);
const prNumber = context.payload.pull_request?.number;
if (!prNumber) {
core.setFailed("Missing pull_request.number in pull_request_target payload.");
return;
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const maxAttempts = 5;
let verdict;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
let latestPr;
try {
latestPr = (
await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
})
).data;
} catch (error) {
core.setFailed(
`Unable to fetch latest PR metadata for PR #${prNumber}: ${error instanceof Error ? error.message : String(error)}. If this was transient, rerun the job.`,
);
return;
}

verdict = classifyMergeability({
mergeable: latestPr.mergeable,
mergeableState: latestPr.mergeable_state,
draft: latestPr.draft,
});

await core.summary
.addHeading("PR mergeability")
.addRaw(`Attempt: ${attempt}/${maxAttempts}<br>`)
.addRaw(`Draft: ${latestPr.draft ? "yes" : "no"}<br>`)
.addRaw(`mergeable: ${String(latestPr.mergeable)}<br>`)
.addRaw(`mergeable_state: ${String(latestPr.mergeable_state)}<br>`)
.addRaw(`Verdict: ${verdict.action} (${verdict.reason})<br>`)
.write();

if (verdict.action !== "retry") break;
if (attempt < maxAttempts) await sleep(3000);
}

if (verdict.action === "retry") {
core.setFailed(
`GitHub did not finish computing mergeability for PR #${prNumber} after ${maxAttempts} attempts. Rerun this check; do not treat a missing CI check list as a pass.`,
);
return;
}

if (verdict.action === "skip") {
core.notice(verdict.message);
return;
}

if (!verdict.ok) {
core.setFailed(verdict.message);
return;
}

core.notice(verdict.message);

refresh-after-base-push:
name: Refresh PR mergeability after base push
if: github.event_name == 'push'
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
checks: write
steps:
# A base push is trusted repository code. Pin the checkout to that exact
# base commit and never fetch or execute an open PR's head.
- name: Checkout trusted base classifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false

- name: Refresh unchanged PR heads
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { pathToFileURL } = require("node:url");
const moduleUrl = pathToFileURL(`${process.env.GITHUB_WORKSPACE}/scripts/pr-mergeability.mjs`).href;
const { classifyMergeability } = await import(moduleUrl);
const base = context.ref.replace(/^refs\/heads\//, "");
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const maxAttempts = 5;
let refreshErrors = 0;

const pulls = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
base,
per_page: 100,
});

for (const listedPr of pulls) {
let latestPr = listedPr;
let verdict;

try {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
latestPr = (
await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: listedPr.number,
})
).data;
verdict = classifyMergeability({
mergeable: latestPr.mergeable,
mergeableState: latestPr.mergeable_state,
draft: latestPr.draft,
});
if (verdict.action !== "retry") break;
if (attempt < maxAttempts) await sleep(3000);
}

const unresolved = verdict?.action === "retry";
const skipped = verdict?.action === "skip";
const conclusion = unresolved ? "failure" : skipped || verdict?.ok ? "success" : "failure";

const reason = unresolved
? `GitHub did not finish computing mergeability after ${maxAttempts} attempts.`
: verdict.message;
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: "PR mergeability",
head_sha: latestPr.head.sha,
status: "completed",
conclusion,
output: {
title: `PR #${latestPr.number}: ${conclusion}`,
summary: [
`Base ${base} advanced to ${context.sha}.`,
`Head remained ${latestPr.head.sha}.`,
`mergeable: ${String(latestPr.mergeable)}`,
`mergeable_state: ${String(latestPr.mergeable_state)}`,
reason,
].join("\n\n"),
},
});
} catch (error) {
refreshErrors += 1;
core.error(
`Unable to refresh PR #${listedPr.number}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}

core.info(`Refreshed PR mergeability for ${pulls.length} open PR(s) targeting ${base}.`);
if (refreshErrors > 0) {
core.setFailed(`${refreshErrors} PR mergeability refresh(es) could not be published.`);
}
55 changes: 55 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,61 @@ several into one PR/session rather than a dedicated branch each.
Bundling saves PR/CI-invocation count, not verification rigor — every bundled item still
gets the smallest correct gate run against it before it joins the PR.

<!-- BEGIN:anti-conflict-speed -->

## Anti-conflict and CI-speed operating procedure

Goal: fewer false merge conflicts, less cancelled CI, and faster feedback — without
weakening required gates, flake policy, provider boundaries, or clinical/RAG safeguards.
Do not touch unrelated active PRs unless the user explicitly asks (`Run PR`, sync, or a
named PR). Future process only.

### Prevent conflicts before they start

- Prefer fewer, shorter-lived PRs. Bundle independently low-risk append-only docs/ledger
chores (see "## PR bundling") instead of one PR per line.
- Start from a fresh `origin/main` worktree/branch (`newtask`); do not pile new work onto a
stale head that already shares hot files with the open queue.
- Treat `docs/branch-review-ledger.md` and `docs/outstanding-issues.md` as hot shared files.
Both use `merge=union`. Append with `npm run ledger:append` / the `/issues` skill — never
hand-write ledger rows, and never resolve an outstanding-issues conflict by taking one side
wholesale. `npm run check:outstanding-issues` fails on duplicate IDs or a stale
`issues:next-id` marker.
- Before calling GitHub `DIRTY`/`CONFLICTING` a real conflict, run
`git merge-tree --write-tree origin/main <tip>`. Clean tree + behind = sync; dirty tree =
real conflict.

### Speed CI without skipping quality

- Assemble every commit for a head before the first push, or wait for the current PR CI run
to settle before pushing again. `cancel-in-progress: true` cancels Production UI mid-flight
on every superseding push (~40% of recent PR CI runs were cancellations).
- Before push: `npm run format` **and commit the result**, then
`npm run verify:pr-local` (or the smallest gate that covers the change). Format is in
`static-pr` but not in `verify:cheap`; an uncommitted format leaves CI red on the pushed
blob. Whole-tree Prettier, not a single edited file.
- If a `claude/*` PR has auto-merge armed, disable it before a settle-then-push bundle, push,
then re-enable — otherwise the first green head can squash-merge before the bundled commit
lands.
- Missing CI checks are not a green pass. `pull_request` workflows do not run when GitHub
cannot build `refs/pull/<n>/merge`. The `PR mergeability` check uses trusted
`pull_request_target` events and refreshes unchanged PR heads after protected-base
pushes; it fails explicitly on `mergeable_state: dirty`. Behind-but-clean heads still use
`npm run sync:pr-branches` / `:apply` with a human `gh` identity — never bot
`update-branch`.
- Keep Playwright blocking tests at zero retries. Quarantine only after three reproductions
on the same SHA via `tests/flake-ledger.json` (`@quarantine`, not `@critical`, ≤30-day
expiry). Do not weaken tap targets to `min-h-11` to chase generic a11y guidance — that
reintroduces a known `ui-smoke` flake.

### Operator sync (explicit only)

- Leave active PRs alone unless the user asks. Report-only inventory:
`npm run sync:pr-branches`. Apply only with confirmation and human/operator `gh` auth:
`npm run sync:pr-branches:apply`.

<!-- END:anti-conflict-speed -->

<!-- BEGIN:codex-productivity-defaults -->

## Codex productivity defaults
Expand Down
3 changes: 3 additions & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-07-30 | PR #1396 / claude/latency-findings-impl-s8g01v | 70e810b66881e17aa9f58126fdad970986bda911 | User ask: resolve comments + Production UI phone-scroll + main sync | FIXED: synced main (DIRTY was staleness); removed union ledger dup; adapted phone-scroll asserts for Answer strategy-overlay + overlay/reserve-only calculator budget + focus pre-scroll inside 8px reveal band. Codex P1s already on tip; 0 unresolved threads. Focused Chromium phone-scroll 9/9 green (system Chrome). | phone-scroll focused 9/9; check:branch-review-ledger PASS; merge-tree clean; prior Codex P1s retained |
| 2026-07-30 | HEAD | 13c16cf07c854b50daa35a2ef2a2ea76d5e059e1 | ci-testing-approach | findings: UI-load flake #093 dominates PR reds; schedule full-sentinel blocks release-browser via audit; UI scope overfires on src/app/api; ~40% PR runs cancelled wasting ~12 UI-hrs; CI_TRIAGE inert; eval:rag:offline claimed-in-CI but only fixtures run | gh-ci-500-runs,ci.yml,ci-change-scope,testing.md,process-hardening,outstanding-issues-093-095-097-023,flake-ledger-empty |
| 2026-07-30 | cursor/ci-testing-review-1bf5 | 13c16cf07c854b50daa35a2ef2a2ea76d5e059e1 | ci-testing-approach | Corrects the ref cell from the unresolved placeholder "HEAD" to the actual branch name, so ledger:lookup can match this review by branch (Codex P2 finding on PR #1406). | node scripts/branch-review-ledger.mjs lookup cursor/ci-testing-review-1bf5 --scope ci-testing-approach |
| 2026-07-30 | cursor/process-anti-conflict-speed-1edf / PR #1416 | 2a31fcee0ef2e330a4901481c0810103d89c96cf | process anti-conflict merge readiness | NOT READY. CI green on stale head, but merge-tree vs current main is CONFLICTING in ci.yml, package.json, and add/add on check-outstanding-issues.mjs after #1410 landed a stronger #112 gate. Keep unique value: AGENTS anti-conflict procedure, #116 PR mergeability workflow, merge=union on outstanding-issues (explicitly still open after #1410). Drop duplicate weaker outstanding-issues checker; re-verify after sync. | ledger:lookup NOT REVIEWED; merge-tree dirty vs origin/main; ManagePullRequest CI SUCCESS (15 ok / 0 fail, Production UI skipped as non-UI); local verify:pr-local earlier on pre-conflict head 4467 passed |
| 2026-07-30 | cursor/process-anti-conflict-speed-1edf / PR #1416 | 387ffd07887f1160fca8fe98c1c4809e852531ae | process anti-conflict merge readiness | READY after sync with main. Kept #1410 structural outstanding-issues gate; added merge=union + runtime attr check; retained #116 PR mergeability workflow and AGENTS anti-conflict playbook; dropped duplicate weaker checker/test. merge-tree clean vs origin/main. | merge-tree clean; check:outstanding-issues pass; check:pr-mergeability pass; check:gate-manifest pass; verify:pr-local pass |
| 2026-07-30 | cursor/process-anti-conflict-speed-1edf / PR #1416 | 38ae07b989e6414026235debad0e429ba64cf462 | process anti-conflict merge readiness | READY at final tip. Same as prior READY plus this ledger append only; merge-tree still clean vs origin/main. | merge-tree clean; verify:pr-local on 387ffd07 parent (4480 passed); tip is ledger-only follow-up |
| 2026-07-30 | cursor/ci-hygiene-gates-1bf5 | ad9da6a6f8ba3884b389fa78e678bb88ee72d9d1 | ci-hygiene-gates | implemented matrix unblock, scope narrow, cancelled≠failure, pinned gitleaks, critical-first UI, eval:rag:offline; skipped #093; verify:cheap 4471 pass | verify:cheap,check:ci-scope,check:gitleaks-pinned,check:gate-manifest,eval:rag:offline |
| 2026-07-30 | cursor/ci-hygiene-gates-1bf5 | b660dbc5a10d7ca3da03541028017f0abc6b5bd3 | ci-hygiene-gates merge-readiness | findings | check:ci-scope;check:gitleaks-pinned;scope-classify PR files ui_changed=false;sim cancelled-as-neutral |
| 2026-07-30 | cursor/ci-hygiene-gates-1bf5 | 8f3283d00da274dee507a1b8e9b611321d1f35be | pr-1413-merge-readiness | READY after main sync + cancel-to-green fix; draft until tip CI green; deferred #093 + CI_TRIAGE_ENABLED confirm | verify:cheap:4481-pass;format:outstanding-issues;merge-tree:clean;cancelled:!cancelled();hosted:awaiting-tip |
Expand Down
Loading
Loading