Skip to content
Open
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
16 changes: 11 additions & 5 deletions .agents/skills/branch-to-merge/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: Use when finishing work on a Coven Cave branch and landing it on pr
Take a finished branch to a merged commit on `main` without ever writing to
`main` directly and without destroying another session's work.

`main` in this repository is protected: pull request required, nine required
`main` in this repository is protected: pull request required, seven required
status checks, no force-push, no deletion. A pull request is
the **only** path an agent may use. This skill is the Cave-specific replacement
for generic "finish a branch" workflows that offer a local merge into the base
Expand Down Expand Up @@ -202,7 +202,7 @@ count.

## Phase 5: Checks and review

Nine required checks must pass:
Seven required checks must pass:

```bash
expected_head=$(git rev-parse HEAD)
Expand All @@ -215,18 +215,24 @@ gh pr view <#> --json headRefOid,mergeable,mergeStateStatus,statusCheckRollup
- `Rust check`
- `E2E (Playwright)`
- `Cross-environment (ubuntu-latest)`
- `Cross-environment (windows-latest)`
- `Cross-environment required`
- `Sidecar runtime (ubuntu-latest)`
- `Sidecar runtime (windows-latest)`
- `Sidecar runtime required`

CodeQL is retired, and code scanning is fully off — nothing scans in its place.
If a required context never reports, the PR sits `BLOCKED` with nothing failing.
Before and after the watch, require `headRefOid` to equal `$expected_head` and
each listed context to be complete and successful. A pass tied to an earlier
SHA, or a pending, cancelled, stale, missing, or failed context, is incomplete;
do not merge until the exact current head has all nine passes.
do not merge until the exact current head has all seven passes.

Windows no longer runs on pull requests, so there is no
`Cross-environment (windows-latest)` or `Sidecar runtime (windows-latest)`
context to wait for. Windows still gates `main`: the two `*-required` rollups
stay required, and the Windows legs run in full on push to `main` and on
release. Do not re-add the windows contexts to protection without first
re-adding the jobs to the pull_request path — a required context that never
reports is the classic `BLOCKED`-with-nothing-failing wedge.

The `E2E (Playwright)` leg runs daemon-less (`COVEN_CAVE_E2E=1`), so e2e specs
must dismiss onboarding and mock APIs via `page.route(...)` rather than expect a
Expand Down
50 changes: 41 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,21 @@ jobs:
# exhausting the org Actions limit and queuing indefinitely, stalling
# every required check. macOS is still covered by the release pipeline
# (release.yml). Restore macos-latest here once macOS budget is sorted.
os: [ubuntu-latest, windows-latest]
#
# Windows now follows macOS off the pull_request path, for the same
# failure and one measurement: on 2026-08-10 the repository held 39
# queued runs with ZERO in progress, the oldest stuck since 2026-07-18,
# while GitHub Actions was globally operational and a hosted runner
# completed an unrelated job mid-stall. The queue, not the suite, was
# the blocker — so every required check on every open PR sat pending.
#
# Windows bills at 2x and was three of the fifteen PR-time jobs. It
# still runs in full on push to main and on release, so `main` keeps
# identical platform coverage; only the per-PR fan-out shrinks. The
# windows-latest leg is deliberately dropped from branch protection in
# the same change — a required context that never reports is exactly
# how a PR ends up BLOCKED with nothing failing (see CLAUDE.md).
os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest","windows-latest"]') }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
Expand Down Expand Up @@ -364,7 +378,7 @@ jobs:
echo "::error::Cross-environment matrix result: ${{ needs.conformance.result }}"
exit 1
fi
echo "Cross-environment matrix passed on ubuntu-latest and windows-latest."
echo "Cross-environment matrix passed on every leg scheduled for this event."

sidecar-runtime:
name: Sidecar runtime (${{ matrix.os }})
Expand All @@ -376,7 +390,10 @@ jobs:
matrix:
# macOS removed from PR CI (see conformance matrix note). Release
# pipeline still exercises macOS. Restore once macOS budget is sorted.
os: [ubuntu-latest, windows-latest]
# Windows likewise runs on push/main and release only — same queue
# saturation, and this is the slowest Windows leg of the three
# (timeout-minutes: 40 against conformance's 15).
os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest","windows-latest"]') }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
Expand Down Expand Up @@ -431,7 +448,11 @@ jobs:
# edit for this job to be able to fail a PR.
windows-native:
name: Windows native tests
if: github.event_name != 'workflow_dispatch' || github.sha == inputs.expected_sha
# Windows runs on push/main and release only (see the conformance matrix
# note). `sidecar-runtime-required` below treats a `skipped` result here as
# acceptable, so a pull_request still gets a reporting rollup rather than a
# rollup that fails on the absence of a job it deliberately did not run.
if: (github.event_name != 'workflow_dispatch' || github.sha == inputs.expected_sha) && github.event_name != 'pull_request'
runs-on: windows-latest
# Hung steps stall required checks for the 6h default (cave-whm5); ~3x p95 (observed ~2.5min incl. cold cargo build).
timeout-minutes: 20
Expand Down Expand Up @@ -561,20 +582,31 @@ jobs:
# without adding a context to branch protection. Both results are
# reported before exiting so one red job does not mask the other.
- name: Require every sidecar runtime matrix leg
env:
EVENT: ${{ github.event_name }}
SIDECAR_RESULT: ${{ needs.sidecar-runtime.result }}
WINDOWS_RESULT: ${{ needs.windows-native.result }}
run: |
failed=0
if [ "${{ needs.sidecar-runtime.result }}" != "success" ]; then
echo "::error::Sidecar runtime matrix result: ${{ needs.sidecar-runtime.result }}"
if [ "$SIDECAR_RESULT" != "success" ]; then
echo "::error::Sidecar runtime matrix result: $SIDECAR_RESULT"
failed=1
fi
if [ "${{ needs.windows-native.result }}" != "success" ]; then
echo "::error::Windows native tests result: ${{ needs.windows-native.result }}"
# Windows native tests do not run on pull_request (they cost a 2x
# runner and the queue could not absorb them; see the conformance
# matrix note). A `skipped` result is therefore expected THERE AND
# ONLY THERE — on push/main a skip is still a hard failure, because
# that is the event whose coverage this rollup exists to guarantee.
if [ "$WINDOWS_RESULT" = "skipped" ] && [ "$EVENT" = "pull_request" ]; then
echo "Windows native tests skipped on pull_request by design; they gate push to main."
elif [ "$WINDOWS_RESULT" != "success" ]; then
echo "::error::Windows native tests result: $WINDOWS_RESULT"
failed=1
fi
if [ "$failed" -ne 0 ]; then
exit 1
fi
echo "Sidecar runtime matrix passed on ubuntu-latest and windows-latest, and Windows native tests passed."
echo "Sidecar runtime matrix passed on every leg scheduled for this event."

# ── iOS ─────────────────────────────────────────────────────────────────────
# The Swift app is compiled NOWHERE in CI. Verified across every workflow:
Expand Down
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ yours. Use a PR.
**Current settings** (verified live; `gh api repos/OpenCoven/coven-cave/branches/main/protection`):

- PR required before merging — **0 approvals** (you can self-merge once checks pass; no second human needed for solo work).
- Required status checks — **all NINE** must pass (widened 2026-08-01 from five): `Frontend build`, `Rust check`, `E2E (Playwright)`, `Cross-environment (ubuntu-latest)`, `Cross-environment (windows-latest)`, `Cross-environment required`, `Sidecar runtime (ubuntu-latest)`, `Sidecar runtime (windows-latest)`, `Sidecar runtime required`. The four matrix legs were added alongside their `*-required` rollups. The rollups already fail unless `needs.<job>.result == 'success'`, so this is defense in depth rather than a gap being closed — it removes the dependency on those aggregation scripts staying correct. Classic branch protection is the active enforcement layer. Ruleset `19123333` lists the same nine checks but is currently disabled, so it does not provide a second gate. Only `ci.yml` runs on `pull_request` and no job carries a skippable `if:`, which is why requiring the legs is safe — a required context that never reports is what leaves a PR stuck `BLOCKED` with nothing failing. **`CodeQL` is retired** (2026-07-31): the ruleset's `code_scanning` rule went first, then the required context in classic branch protection, and now the workflow itself. Code scanning is fully off — GitHub default setup is `not-configured`, so nothing scans in its place. If you ever see a PR stuck `BLOCKED` with `mergeable: MERGEABLE`, no failing check and every conversation resolved, check two things: a required context that no longer reports (compare `gh api repos/OpenCoven/coven-cave/branches/main/protection --jq .required_status_checks.contexts` against the checks the PR actually runs), and `required_signatures` (see the signatures bullet below — it produced exactly this symptom on three PRs and is now off). The `E2E (Playwright)` job runs daemon-less (`COVEN_CAVE_E2E=1`), so e2e specs must be self-contained — dismiss onboarding (`cave:onboarding:dismissed=1`) and drive surfaces via `page.route(...)` API mocks rather than a live daemon.
- Required status checks — **all SEVEN** must pass (widened 2026-08-01 from five to nine, narrowed 2026-08-10 to seven when Windows left the pull_request path): `Frontend build`, `Rust check`, `E2E (Playwright)`, `Cross-environment (ubuntu-latest)`, `Cross-environment required`, `Sidecar runtime (ubuntu-latest)`, `Sidecar runtime required`. The two ubuntu matrix legs are required alongside their `*-required` rollups. The rollups already fail unless the legs they aggregate succeeded, so this is defense in depth rather than a gap being closed — it removes the dependency on those aggregation scripts staying correct. Classic branch protection is the active enforcement layer. Ruleset `19123333` is currently disabled, so it does not provide a second gate. Only `ci.yml` runs on `pull_request`.

⚠️ **Only require a context that always reports on a pull request.** The two `(windows-latest)` legs were required until 2026-08-10; they were dropped in the same change that took Windows off the `pull_request` path, because a required context that never reports is precisely what leaves a PR stuck `BLOCKED` with nothing failing. Windows still gates `main` — `conformance`, `sidecar-runtime`, and `windows-native` all run in full on push to `main`, and `release.yml` builds the Windows MSI — but on a pull request `windows-native` is deliberately skipped and the conformance/sidecar matrices are ubuntu-only. `Sidecar runtime required` therefore accepts a `skipped` `windows-native` **on `pull_request` only**, and still hard-fails a skip on any other event. `scripts/ci-recovery-workflow.test.mjs` pins all of that, so re-adding a windows context to protection without first re-adding the jobs will fail the suite before it can wedge a PR. **`CodeQL` is retired** (2026-07-31): the ruleset's `code_scanning` rule went first, then the required context in classic branch protection, and now the workflow itself. Code scanning is fully off — GitHub default setup is `not-configured`, so nothing scans in its place. If you ever see a PR stuck `BLOCKED` with `mergeable: MERGEABLE`, no failing check and every conversation resolved, check two things: a required context that no longer reports (compare `gh api repos/OpenCoven/coven-cave/branches/main/protection --jq .required_status_checks.contexts` against the checks the PR actually runs), and `required_signatures` (see the signatures bullet below — it produced exactly this symptom on three PRs and is now off). The `E2E (Playwright)` job runs daemon-less (`COVEN_CAVE_E2E=1`), so e2e specs must be self-contained — dismiss onboarding (`cave:onboarding:dismissed=1`) and drive surfaces via `page.route(...)` API mocks rather than a live daemon.

A separate scheduled workflow detects GitHub event-delivery gaps: after a
15-minute grace period from the PR's latest update it dispatches a fresh
Expand All @@ -50,7 +52,7 @@ yours. Use a PR.

Prefer this fresh dispatch to rerunning a queued run with zero jobs: there is
no job to rerun, and the stalled run can remain queued. Recovery does not
bypass branch protection; all nine required contexts still have to report and
bypass branch protection; all seven required contexts still have to report and
pass on the exact PR head.
- Review conversations are **no longer required to be resolved**
(`required_conversation_resolution` was turned OFF on 2026-08-01, at the
Expand Down Expand Up @@ -136,7 +138,7 @@ yours. Use a PR.
If you believe it should change, say so to the owner and leave it alone.

Nothing here changes how **agents** land work. Every rule below still binds
us: work on a branch, open a PR, wait for the nine required checks. The
us: work on a branch, open a PR, wait for the seven required checks. The
`--admin` flag `gh` dangles at you on a blocked merge is still not the fix —
fix the actual blocker.
- Force-pushes and deletion of `main` are blocked. `allow_deletions = false`
Expand Down
2 changes: 1 addition & 1 deletion apps/ios/CovenCave/CovenCave/Views/ConnectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ struct ConnectionView: View {
switch outcome {
case .found(let url):
liveCheck = .found(port: url.port)
case .unauthorized:
case .unauthorized, .credentialFailure(_):
liveCheck = .pairingRequired
case .unreachable(let failure):
liveCheck = .failed(failure)
Expand Down
2 changes: 1 addition & 1 deletion scripts/branch-to-merge-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function backticked(source) {

function documentedChecks() {
const bullet =
/- Required status checks — \*\*all ([A-Z]+)\*\* must pass[^:]*:(.*?)\. The four matrix legs/s.exec(
/- Required status checks — \*\*all ([A-Z]+)\*\* must pass[^:]*:(.*?)\. The two ubuntu matrix legs/s.exec(
claude,
);
assert.ok(bullet, "CLAUDE.md no longer states the required status checks in the expected shape");
Expand Down
68 changes: 65 additions & 3 deletions scripts/ci-recovery-workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@ assert.equal(
"ci-${{ github.event.pull_request.head.sha || inputs.expected_sha || github.sha }}",
"late pull_request delivery and its recovery dispatch must share one concurrency key",
);
const SHA_GUARD = "github.event_name != 'workflow_dispatch' || github.sha == inputs.expected_sha";

// Every job must refuse a recovery dispatch whose branch head has moved. A job
// may narrow itself further (for example, skipping on pull_request to keep a
// paid runner off the PR fan-out), but only by ANDing extra conditions onto the
// guard. Any `||` in the suffix could re-admit a stale dispatch, so the shape is
// pinned rather than merely searched for.
function assertShaGuarded(jobName, condition) {
assert.equal(typeof condition, "string", `${jobName} must declare an if: condition`);
if (condition === SHA_GUARD) {
return;
}
const prefix = `(${SHA_GUARD}) && `;
assert.ok(
condition.startsWith(prefix),
`${jobName} if: condition must start with the SHA guard prefix before adding extra conditions`,
);
const suffix = condition.slice(prefix.length);
assert.ok(suffix.length > 0, `${jobName} must not AND the guard against an empty condition`);
assert.ok(
!suffix.includes("||"),
`${jobName} must not weaken the head-moved guard with any disjunction in the suffix`,
);
}

for (const jobName of [
"frontend-static",
"frontend-tests",
Expand All @@ -65,11 +90,48 @@ for (const jobName of [
"sidecar-runtime",
"windows-native",
]) {
assertShaGuarded(jobName, ciWorkflow.jobs[jobName].if);
}

// Paid-runner fan-out policy: Windows is billed at 2x and the shared Actions
// queue could not absorb three Windows legs per pull request. Windows coverage
// moves to push/main and release. These assertions exist so the reduction
// cannot be silently undone, and so the rollup below cannot be "fixed" by
// accepting a skip on the very event whose coverage it guarantees.
const PR_ONLY_UBUNTU =
"${{ github.event_name == 'pull_request' && fromJSON('[\"ubuntu-latest\"]')" +
" || fromJSON('[\"ubuntu-latest\",\"windows-latest\"]') }}";
for (const jobName of ["conformance", "sidecar-runtime"]) {
assert.equal(
ciWorkflow.jobs[jobName].if,
"github.event_name != 'workflow_dispatch' || github.sha == inputs.expected_sha",
`${jobName} must not run a recovery dispatch after the branch head moves`,
ciWorkflow.jobs[jobName].strategy.matrix.os,
PR_ONLY_UBUNTU,
`${jobName} must run ubuntu-only on pull_request and ubuntu+windows elsewhere`,
);
}
assert.ok(
ciWorkflow.jobs["windows-native"].if.includes("github.event_name != 'pull_request'"),
"windows-native must stay off the pull_request fan-out",
);

const sidecarRollup = ciWorkflow.jobs["sidecar-runtime-required"];
assert.deepEqual(
sidecarRollup.needs,
["sidecar-runtime", "windows-native"],
"the sidecar rollup must still depend on the Windows legs it reports for",
);
const sidecarGate = sidecarRollup.steps.find(
(step) => step.name === "Require every sidecar runtime matrix leg",
);
assert.ok(sidecarGate, "the sidecar rollup must gate on its matrix legs");
assert.match(
sidecarGate.run,
/"\$WINDOWS_RESULT" = "skipped" \] && \[ "\$EVENT" = "pull_request"/,
"a skipped windows-native is acceptable on pull_request only",
);
assert.match(
sidecarGate.run,
/elif \[ "\$WINDOWS_RESULT" != "success" \]/,
"any other windows-native result, on any other event, must fail the rollup",
);

console.log("ci-recovery-workflow.test.mjs: ok");
6 changes: 4 additions & 2 deletions scripts/dependency-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ assert.equal(
// under the other version's JS → native/JS ABI mismatch → on win32 `format()`
// returns a table with no `heif`, so `sharp/dist/utility.cjs` throws at load and
// the avatar route 500s. macOS/Linux tolerate the mismatch, so ONLY the
// `Sidecar runtime (windows-latest)` CI leg catches it. This guard fails fast in
// the required Frontend-build check instead. Fix a divergence by aligning `sharp`
// `Sidecar runtime (windows-latest)` CI leg catches it — and since 2026-08-10 that
// leg runs on push to `main` and on release, NOT on pull requests. So this guard is
// now the only pre-merge signal for the skew; it fails fast in the required
// Frontend build check. Fix a divergence by aligning `sharp`
// to next's pin, or by adding `pnpm.overrides.sharp` so next's transitive copy is
// forced to the same version. (PR #2263 dug out this root cause.)
const lockfile = parse(await readFile(new URL("../pnpm-lock.yaml", import.meta.url), "utf8"));
Expand Down
Loading