From 5b43b37cd9b6068651afed9fc28c25663545c38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 14 Oct 2026 19:52:27 +0200 Subject: [PATCH 1/5] ci: skip device lanes on drafts and spend the mutation canary only on lane sources Device smoke lanes (iOS, macOS, Android, Linux) skip draft PRs and start on ready_for_review; a gate test holds every PR smoke lane to both. The mutation canary splits LANE_TOOLING into LANE_SOURCES, which spend mutants on every PR, and LANE_DEPENDENCIES (root and workspace package.json, the lockfile), which run the harness self-test and spend mutants only on manual dispatch with --with-dependencies. Five of the seven PRs that recently ran the canary touched only workspace package.json files. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/android.yml | 2 + .github/workflows/ios.yml | 2 + .github/workflows/linux.yml | 2 + .github/workflows/macos.yml | 2 + .github/workflows/mutation-affected.yml | 28 ++++++++++---- scripts/gate/workflows.test.ts | 34 +++++++++++++++++ scripts/mutation/run.ts | 51 ++++++++++++++++--------- scripts/mutation/selection.test.ts | 26 ++++++++++--- scripts/mutation/workflow.test.ts | 26 ++++++++++--- 9 files changed, 136 insertions(+), 37 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 2f89157167..376d328a30 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,6 +2,7 @@ name: Android on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths-ignore: - 'docs/**' - 'website/**' @@ -30,6 +31,7 @@ concurrency: jobs: smoke-android: name: Smoke Tests + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest timeout-minutes: 80 steps: diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 71f851fe41..ea5f5a4506 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -2,6 +2,7 @@ name: iOS on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths-ignore: - 'docs/**' - 'website/**' @@ -61,6 +62,7 @@ concurrency: jobs: smoke-ios: name: Smoke Tests + if: ${{ !github.event.pull_request.draft }} runs-on: macos-26 timeout-minutes: 80 env: diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2d1c29d629..6b1194ae84 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -2,6 +2,7 @@ name: Linux on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths-ignore: - 'docs/**' - 'website/**' @@ -29,6 +30,7 @@ concurrency: jobs: smoke-linux: name: Smoke Tests + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest timeout-minutes: 30 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 9d683a3c02..eb7022682b 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -2,6 +2,7 @@ name: macOS on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths-ignore: - 'docs/**' - 'website/**' @@ -49,6 +50,7 @@ jobs: # goes. smoke-macos: name: Smoke Tests + if: ${{ !github.event.pull_request.draft }} runs-on: macos-26 timeout-minutes: 80 env: diff --git a/.github/workflows/mutation-affected.yml b/.github/workflows/mutation-affected.yml index 6886d8f5bc..2a79509782 100644 --- a/.github/workflows/mutation-affected.yml +++ b/.github/workflows/mutation-affected.yml @@ -8,9 +8,12 @@ name: Mutation Harness Canary # Cost control: the kernel report that pays is the weekly sweep, so this lane # spends mutants on one thing only — proving the harness still runs end to end # when the harness itself changes. `select` therefore returns an empty matrix -# unless the diff touches the lane's own sources. Selecting on derived kernel -# ownership instead would run the full sweep on 24 of the last 40 -# merged PRs. +# unless the diff touches the lane's own sources (LANE_SOURCES). Selecting on +# derived kernel ownership instead would run the full sweep on 24 of the last 40 +# merged PRs. A diff confined to LANE_DEPENDENCIES runs the self-test only; most +# such diffs are unrelated workspace edits. Dispatch on the branch to spend the +# canary (pass -f base= for a stacked branch): +# gh workflow run mutation-affected.yml --ref # # On a harness diff the matrix is the canary (`LANE_CANARY`, selected by # observed end-to-end runtime within the job budget — lane sources own no kernel, @@ -22,8 +25,7 @@ name: Mutation Harness Canary on: pull_request: paths: - # Exactly `LANE_TOOLING` in scripts/mutation/run.ts — the - # only diffs that can produce a non-empty matrix, asserted both ways by + # Exactly `LANE_TOOLING` in scripts/mutation/run.ts, asserted both ways by # scripts/mutation/workflow.test.ts. A kernel or test path here would only # start a job that selects nothing. - 'scripts/mutation/**' @@ -36,6 +38,11 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'packages/*/package.json' + workflow_dispatch: + inputs: + base: + description: Branch the diff is measured against + default: main permissions: contents: read @@ -44,6 +51,10 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + MUTATION_BASE: origin/${{ github.event.pull_request.base.ref || inputs.base }} + MUTATION_SCOPE: ${{ github.event_name == 'workflow_dispatch' && '--with-dependencies' || '' }} + jobs: select: name: Select harness validation kernels @@ -72,7 +83,8 @@ jobs: args: | --list-affected --base - origin/${{ github.event.pull_request.base.ref }} + ${{ env.MUTATION_BASE }} + ${{ env.MUTATION_SCOPE }} # The self-test and the derivation run before any mutant, so a failure here # would leave the lane with no envelope at all (#1430). @@ -170,9 +182,9 @@ jobs: expected=$(echo '${{ needs.select.outputs.modules }}' | jq length) pnpm gate mutation-check --report-dir .tmp/mutation/shards --affected \ --expect-shards "$expected" \ - --base "origin/${{ github.event.pull_request.base.ref }}" + --base "$MUTATION_BASE" $MUTATION_SCOPE else - pnpm gate mutation-affected --base "origin/${{ github.event.pull_request.base.ref }}" + pnpm gate mutation-affected --base "$MUTATION_BASE" $MUTATION_SCOPE fi - name: Record a failed lane envelope diff --git a/scripts/gate/workflows.test.ts b/scripts/gate/workflows.test.ts index a73f6b207a..353277f47c 100644 --- a/scripts/gate/workflows.test.ts +++ b/scripts/gate/workflows.test.ts @@ -291,3 +291,37 @@ on: }); assert.equal(lanes.find((lane) => lane.workflow === 'planted.yml')?.unsupported.length, 1); }); + +// Drafts are where agents iterate; a smoke lane that runs on them spends device +// runners on heads nobody is reviewing, and one that misses `ready_for_review` +// never runs on a draft marked ready until its next push. +test('device smoke lanes skip drafts and start when a draft is marked ready', () => { + const directory = path.join(repoRoot, '.github/workflows'); + type Workflow = { + name: string; + on: { pull_request?: { types?: string[] } }; + jobs: Record; + }; + const smokeLanes = fs + .readdirSync(directory) + .map((file) => parse(fs.readFileSync(path.join(directory, file), 'utf8')) as Workflow) + .flatMap((workflow) => + Object.values(workflow.jobs) + .filter((job) => job.name === 'Smoke Tests' && workflow.on.pull_request) + .map((job) => ({ workflow, job })), + ); + assert.deepEqual(smokeLanes.map(({ workflow }) => workflow.name).sort(), [ + 'Android', + 'Linux', + 'iOS', + 'macOS', + ]); + for (const { workflow, job } of smokeLanes) { + assert.deepEqual( + workflow.on.pull_request?.types, + ['opened', 'synchronize', 'reopened', 'ready_for_review'], + workflow.name, + ); + assert.equal(job.if, '${{ !github.event.pull_request.draft }}', workflow.name); + } +}); diff --git a/scripts/mutation/run.ts b/scripts/mutation/run.ts index 52ce041411..d661e06022 100644 --- a/scripts/mutation/run.ts +++ b/scripts/mutation/run.ts @@ -78,7 +78,10 @@ const USAGE = `Usage: pnpm mutation:run [options] --summary Also write the markdown report to --no-run Alias for --report with the default report path --list-affected Print the PR lane's shard matrix as JSON and exit (empty - unless the diff touches the lane's own tooling) + unless the diff touches the lane's own sources) + --with-dependencies + Also select the canary for a diff touching only the lane's + dependency manifests --fail-envelope Write a failed lane envelope for a step that ran before (or instead of) the sweep, e.g. a self-test failure @@ -114,6 +117,7 @@ type Args = { reportDir: string | undefined; summary: string | undefined; listAffected: boolean; + withDependencies: boolean; failEnvelope: string | undefined; shard: Shard | undefined; expectShards: number | undefined; @@ -159,6 +163,7 @@ function parseMutationArgs(argv: readonly string[]): Args { summary: { type: 'string' }, 'no-run': { type: 'boolean', default: false }, 'list-affected': { type: 'boolean', default: false }, + 'with-dependencies': { type: 'boolean', default: false }, 'fail-envelope': { type: 'string' }, shard: { type: 'string' }, 'expect-shards': { type: 'string' }, @@ -175,6 +180,7 @@ function parseMutationArgs(argv: readonly string[]): Args { reportDir: values['report-dir'], summary: values.summary, listAffected: Boolean(values['list-affected']), + withDependencies: Boolean(values['with-dependencies']), failEnvelope: values['fail-envelope'], shard: parseShard(values.shard), expectShards: values['expect-shards'] ? Number(values['expect-shards']) : undefined, @@ -605,7 +611,7 @@ async function sweep(args: Args, state: LaneState): Promise { // Same selection the PR matrix uses, so the reporting job can never score // mutants the `select` job decided not to spend. if (args.affected) { - state.modules = [...new Set(affectedMatrix(args.base).map((entry) => entry.module))]; + state.modules = [...new Set(affectedMatrix(args).map((entry) => entry.module))]; } if (state.modules.length === 0) { process.stdout.write('mutation: no decision-kernel modules affected — nothing to mutate.\n'); @@ -645,14 +651,8 @@ async function sweep(args: Args, state: LaneState): Promise { return 0; } -/** - * Sources of the lane itself: a change here must prove itself on real mutants. - * These are also the only paths that can produce a non-empty matrix, so - * `mutation-affected.yml` triggers on exactly them — asserted by - * `workflow.test.ts`, since a filter that missed one would let a harness change - * merge unproven, and any wider filter only buys a no-op job. - */ -export const LANE_TOOLING = [ +/** Code and config the lane executes: a change here proves itself on real mutants on every PR. */ +const LANE_SOURCES = [ 'scripts/mutation/', 'scripts/lib/', 'stryker.config.json', @@ -660,11 +660,23 @@ export const LANE_TOOLING = [ 'vitest.config.ts', '.github/workflows/mutation-weekly.yml', '.github/workflows/mutation-affected.yml', - 'package.json', - 'pnpm-lock.yaml', - 'packages/*/package.json', ]; +/** + * Manifests the lane resolves through. Unrelated PRs touch these routinely (a + * workspace export, a root script), so a diff confined to them spends mutants + * only under `--with-dependencies`, which the workflow's manual dispatch passes. + */ +const LANE_DEPENDENCIES = ['package.json', 'pnpm-lock.yaml', 'packages/*/package.json']; + +/** + * `mutation-affected.yml` triggers on exactly these, asserted by + * `workflow.test.ts`: sources spend the canary, and a dependency-only diff runs + * the harness self-test. A filter that missed one would let that change merge + * unproven. + */ +export const LANE_TOOLING = [...LANE_SOURCES, ...LANE_DEPENDENCIES]; + /** * The PR lane's matrix: empty unless the diff touches the harness, otherwise the * canary plus whatever kernels that same diff derives. The kernel report that @@ -675,23 +687,24 @@ export const LANE_TOOLING = [ */ export function affectedMatrixFor( changed: readonly string[], - root: string = repoRoot, + { withDependencies = false }: { withDependencies?: boolean } = {}, ): ShardSpec[] { + const lanePaths = withDependencies ? LANE_TOOLING : LANE_SOURCES; const touchesLane = changed.some((file) => - LANE_TOOLING.some((pattern) => + lanePaths.some((pattern) => path.matchesGlob(normalizePath(file), pattern.endsWith('/') ? `${pattern}**` : pattern), ), ); if (!touchesLane) return []; - const modules = new Set(derivedAffectedModules(changed, root)); + const modules = new Set(derivedAffectedModules(changed, repoRoot)); // Lane sources own no kernel, so a tooling-only diff derives nothing: without // the canary the lane would select zero mutants and prove nothing. modules.add(LANE_CANARY); return shardMatrix(ALL_MODULE_IDS.filter((id) => modules.has(id))); } -function affectedMatrix(base: string): ShardSpec[] { - return affectedMatrixFor(changedFiles(base)); +function affectedMatrix({ base, withDependencies }: Args): ShardSpec[] { + return affectedMatrixFor(changedFiles(base), { withDependencies }); } /** @@ -726,7 +739,7 @@ async function main(argv = process.argv.slice(2)): Promise { // Selection only — no lane run, so no envelope. The shard jobs that consume // this matrix each write their own. const args = parseMutationArgs(argv); - process.stdout.write(`${JSON.stringify(affectedMatrix(args.base))}\n`); + process.stdout.write(`${JSON.stringify(affectedMatrix(args))}\n`); return 0; } const state: LaneState = { diff --git a/scripts/mutation/selection.test.ts b/scripts/mutation/selection.test.ts index 1725105b25..fbe9f15145 100644 --- a/scripts/mutation/selection.test.ts +++ b/scripts/mutation/selection.test.ts @@ -54,7 +54,7 @@ function worktreeWithCommit(name: string, files: readonly string[]): string { return dir; } -function listAffected(cwd: string): ShardSpec[] { +function listAffected(cwd: string, extraArgs: readonly string[] = []): ShardSpec[] { const result = runCmdSync( 'node', [ @@ -63,6 +63,7 @@ function listAffected(cwd: string): ShardSpec[] { '--list-affected', '--base', 'HEAD~1', + ...extraArgs, ], { cwd }, ); @@ -101,16 +102,31 @@ test('non-harness paths, including near-miss manifests, select nothing', () => { } }); -test('workflow, Vitest, dependency, and workspace manifest changes select the harness canary', () => { +test('lane code, config, and workflow changes select the harness canary', () => { for (const file of [ + 'scripts/lib/cli-args.ts', + 'stryker.config.json', 'vitest.mutation.config.ts', 'vitest.config.ts', '.github/workflows/mutation-weekly.yml', '.github/workflows/mutation-affected.yml', - 'package.json', - 'pnpm-lock.yaml', - 'packages/kernel/package.json', ]) { assert.deepEqual(affectedMatrixFor([file]), shardMatrix([LANE_CANARY]), file); } }); + +// Workspace manifests change in most cross-package PRs; spending the canary on +// each one is what the manual dispatch exists to avoid. +test('dependency and workspace manifest changes select the canary only with --with-dependencies', () => { + for (const file of ['package.json', 'pnpm-lock.yaml', 'packages/kernel/package.json']) { + assert.deepEqual(affectedMatrixFor([file]), [], file); + assert.deepEqual( + affectedMatrixFor([file], { withDependencies: true }), + shardMatrix([LANE_CANARY]), + file, + ); + } + const dir = worktreeWithCommit('manifest', ['packages/kernel/package.json']); + assert.deepEqual(listAffected(dir), []); + assert.deepEqual(listAffected(dir, ['--with-dependencies']), shardMatrix([LANE_CANARY])); +}); diff --git a/scripts/mutation/workflow.test.ts b/scripts/mutation/workflow.test.ts index 5ce7774022..0193e90d4e 100644 --- a/scripts/mutation/workflow.test.ts +++ b/scripts/mutation/workflow.test.ts @@ -58,11 +58,10 @@ test('no mutation shard is allowed to exceed the 30-minute budget', () => { } }); -// Only a harness diff can produce a non-empty matrix, so the trigger is asserted -// in both directions against LANE_TOOLING: a missing path lets a harness change -// merge without ever running a mutant, and an extra one starts a select job that -// can only answer `[]`. -test('the affected lane triggers on exactly the lane sources that can select mutants', () => { +// Asserted in both directions against LANE_TOOLING: a missing path lets a lane +// change merge without its self-test or mutants, and an extra one starts a select +// job that can only answer `[]`. +test('the affected lane triggers on exactly the lane tooling', () => { // Quote style is the formatter's business (oxfmt formats the workflow tree), so // accept either spelling of the same scalar rather than pinning this gate to it. const paths = [ @@ -77,6 +76,23 @@ test('the affected lane triggers on exactly the lane sources that can select mut assert.match(workflow('mutation-affected.yml'), /gate: mutation-affected[\s\S]*--list-affected/); }); +// Selection and scoring must agree on scope, or the report would score shards the +// select job never spent (or expect ones it skipped). +test('manual dispatch widens selection and scoring to the lane dependencies', () => { + const yaml = parse(workflow('mutation-affected.yml')); + assert.ok('workflow_dispatch' in yaml.on, 'dependency-only diffs have no way to run the canary'); + assert.equal( + yaml.env.MUTATION_SCOPE, + "${{ github.event_name == 'workflow_dispatch' && '--with-dependencies' || '' }}", + ); + const select = yaml.jobs.select.steps.find((step: { id?: string }) => step.id === 'select'); + assert.match(select.with.args, /\$\{\{ env\.MUTATION_SCOPE \}\}/); + const score = yaml.jobs.report.steps.find( + (step: { name: string }) => step.name === 'Score the affected modules', + ); + assert.equal(score.run.match(/--base "\$MUTATION_BASE" \$MUTATION_SCOPE/g)?.length, 2); +}); + test('mutant job budgets include setup and unconditional evidence upload', () => { for (const name of ['mutation-weekly.yml', 'mutation-affected.yml']) { const jobs = parse(workflow(name)).jobs; From 885d9a6493bab3120ed9f750e4bad9ed0232cecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 14 Oct 2026 19:52:27 +0200 Subject: [PATCH 2/5] docs(agents): push once per review round and document the canary dispatch Co-Authored-By: Claude Opus 5.5 --- docs/agents/pull-requests.md | 5 +++-- docs/agents/testing.md | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/agents/pull-requests.md b/docs/agents/pull-requests.md index 9a559dc1ec..985d89091a 100644 --- a/docs/agents/pull-requests.md +++ b/docs/agents/pull-requests.md @@ -16,14 +16,15 @@ reported. If a gate fails, diagnose it and rerun after the cause is resolved; re failures without claiming validation passed. Rerun a failed CI job once only when its log shows an infrastructure fault (runner startup, network, device boot); the same failure twice is a finding. +Push once per review round, not per fix: each push restarts every lane. Drafts skip device lanes. + - **Published**: branch pushed, PR opened, and validation recorded at a named commit. - **Merge-ready**: required checks pass on the actual head, with live evidence for device-facing changes. Docs-only and pure-tooling changes do not need device runs. Reporting publication does not require waiting for CI. A local unit run does not establish provider integration or coverage. When those checks are -selected, verify the **Integration Tests** and **Coverage** jobs on the PR head. Run -`pnpm check:fallow --base origin/main` when code-quality or dead-code risk warrants it. +selected, verify the **Integration Tests** and **Coverage** jobs on the PR head. For device-facing changes, fixtures do not replace a live run whose output proves it reached the route the change adds or modifies; a neighbouring route is not evidence. If blocked, record the diff --git a/docs/agents/testing.md b/docs/agents/testing.md index 769ddf257c..2b83dfe565 100644 --- a/docs/agents/testing.md +++ b/docs/agents/testing.md @@ -124,8 +124,8 @@ worker process, so a case that never returns or kills its process is reported as against the exact input. Promote a discovered case with the command the harness prints — never hand-copy an unshrunk input. -Mutation scope: `scripts/mutation/modules.ts#KERNEL_MODULES` (weekly), `scripts/mutation/run.ts#LANE_TOOLING` -(PR canary). Scores are advisory; incomplete sweeps fail. Pilot protocol: `pnpm mutation:run --help`. +Mutation scope: `scripts/mutation/modules.ts#KERNEL_MODULES` (weekly), `scripts/mutation/run.ts#LANE_SOURCES` +(PR canary; `LANE_DEPENDENCIES` on dispatch). Scores are advisory; incomplete sweeps fail. Pilot protocol: `pnpm mutation:run --help`. ## Before editing a shared module @@ -145,8 +145,7 @@ selects gates independently from the diff; it does not consume the depgraph repo action with a literal gate id; `pnpm check:gate-manifest` verifies registration, workflow ownership, path reachability, and routed device lanes. Raw shell text cannot declare ownership. -New check: update the catalog and its executable model. Changed path ownership: plant a path that -would previously be misrouted and watch the selector or manifest fail before fixing it. +New check: update the catalog and its executable model. ## Concurrency torture lane From 175a8deafba6e4e6be67fe76ec5b3731507ca897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 14 Oct 2026 19:52:42 +0200 Subject: [PATCH 3/5] docs(agents): allow 2,500-line PRs for one coherent change Co-Authored-By: Claude Opus 5.5 --- docs/agents/pull-requests.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/agents/pull-requests.md b/docs/agents/pull-requests.md index 985d89091a..b904d01bf1 100644 --- a/docs/agents/pull-requests.md +++ b/docs/agents/pull-requests.md @@ -31,7 +31,8 @@ route the change adds or modifies; a neighbouring route is not evidence. If bloc command and device needed and the remaining risk. Close manual sessions using `docs/agents/device-verification.md` and report incomplete cleanup. -Gross diff budget: 1,000 lines by `git diff --stat origin/main...HEAD`. Rename-only move PRs titled +Gross diff budget: 1,000 lines by `git diff --stat origin/main...HEAD`; 2,500 for one coherent change +in self-contained commits. Stack only layers that land independently. Rename-only move PRs titled `refactor(move)` are exempt when `git diff -M90% --stat origin/main...HEAD` proves no material content change. From 3ad6b775bc3433cdac8be6494359f67fee9d29bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 14 Oct 2026 20:24:28 +0200 Subject: [PATCH 4/5] ci: cancel in-flight smoke lanes when a PR returns to draft Co-Authored-By: Claude Opus 5.5 --- .github/workflows/android.yml | 2 +- .github/workflows/ios.yml | 2 +- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 2 +- scripts/gate/workflows.test.ts | 5 +++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 376d328a30..d498bc01b5 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,7 +2,7 @@ name: Android on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths-ignore: - 'docs/**' - 'website/**' diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index ea5f5a4506..acd8e4dcc2 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -2,7 +2,7 @@ name: iOS on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths-ignore: - 'docs/**' - 'website/**' diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 6b1194ae84..5b320dccbf 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -2,7 +2,7 @@ name: Linux on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths-ignore: - 'docs/**' - 'website/**' diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index eb7022682b..38d3deff41 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -2,7 +2,7 @@ name: macOS on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] paths-ignore: - 'docs/**' - 'website/**' diff --git a/scripts/gate/workflows.test.ts b/scripts/gate/workflows.test.ts index 353277f47c..21c43e8919 100644 --- a/scripts/gate/workflows.test.ts +++ b/scripts/gate/workflows.test.ts @@ -294,7 +294,8 @@ on: // Drafts are where agents iterate; a smoke lane that runs on them spends device // runners on heads nobody is reviewing, and one that misses `ready_for_review` -// never runs on a draft marked ready until its next push. +// never runs on a draft marked ready until its next push. `converted_to_draft` +// starts a skipped run whose concurrency group cancels the one in flight. test('device smoke lanes skip drafts and start when a draft is marked ready', () => { const directory = path.join(repoRoot, '.github/workflows'); type Workflow = { @@ -319,7 +320,7 @@ test('device smoke lanes skip drafts and start when a draft is marked ready', () for (const { workflow, job } of smokeLanes) { assert.deepEqual( workflow.on.pull_request?.types, - ['opened', 'synchronize', 'reopened', 'ready_for_review'], + ['opened', 'synchronize', 'reopened', 'ready_for_review', 'converted_to_draft'], workflow.name, ); assert.equal(job.if, '${{ !github.event.pull_request.draft }}', workflow.name); From 2a4c15cb16132c31c1f7adaf9899c5c3a15b829f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 14 Oct 2026 20:24:28 +0200 Subject: [PATCH 5/5] docs(agents): state the push and canary-dispatch scope precisely Co-Authored-By: Claude Opus 5.5 --- docs/agents/pull-requests.md | 2 +- docs/agents/testing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/agents/pull-requests.md b/docs/agents/pull-requests.md index b904d01bf1..c44e7746e6 100644 --- a/docs/agents/pull-requests.md +++ b/docs/agents/pull-requests.md @@ -16,7 +16,7 @@ reported. If a gate fails, diagnose it and rerun after the cause is resolved; re failures without claiming validation passed. Rerun a failed CI job once only when its log shows an infrastructure fault (runner startup, network, device boot); the same failure twice is a finding. -Push once per review round, not per fix: each push restarts every lane. Drafts skip device lanes. +Push once per review round, not per fix: each push restarts CI. Drafts skip device lanes. - **Published**: branch pushed, PR opened, and validation recorded at a named commit. - **Merge-ready**: required checks pass on the actual head, with live evidence for device-facing diff --git a/docs/agents/testing.md b/docs/agents/testing.md index 2b83dfe565..b42194f5d5 100644 --- a/docs/agents/testing.md +++ b/docs/agents/testing.md @@ -125,7 +125,7 @@ against the exact input. Promote a discovered case with the command the harness hand-copy an unshrunk input. Mutation scope: `scripts/mutation/modules.ts#KERNEL_MODULES` (weekly), `scripts/mutation/run.ts#LANE_SOURCES` -(PR canary; `LANE_DEPENDENCIES` on dispatch). Scores are advisory; incomplete sweeps fail. Pilot protocol: `pnpm mutation:run --help`. +(PR canary; dispatch adds `LANE_DEPENDENCIES`). Scores are advisory; incomplete sweeps fail. Pilot protocol: `pnpm mutation:run --help`. ## Before editing a shared module