AGENTS.md@.github/workflows · git:20261018.67883b8 · 2026-10-18 · sha256 4e9aceeaf6289a51
AGENTS.md@.github/workflows git:20261018.67883b8A
Immutable. This exact content never changes and is served at /api/v1/blob/4e9aceeaf6289a51.
# DOX — .github/workflows
Files in this directory. One row per file. Non-source area (migrated from `docs/file-index-skills-misc.md`; source of truth now here). See change: migrate-file-index-to-agents-tree.
| File | Purpose |
|------|---------|
| `_electron-build.yml` | Reusable workflow_call. Encapsulates 6-leg Electron matrix (darwin/linux/win32 × arm64/x64) with per-leg… Top-level `contents: read`; `use_dependency_cache` input (default `false`) gates the pnpm cache. → see `_electron-build.yml.AGENTS.md` |
| `_smoke.yml` | Reusable `workflow_call`. Encapsulates 7-leg standalone-install-smoke matrix: Linux × 6 (Node 22/24/25 × bookworm-slim/alpine) + Windows × 1 (Node 22). Input `ref` (string, required) — git ref to check out. Consumers: `ci-smoke.yml` (manual dispatch), `publish.yml` `smoke` job (release gate). Pure artifact-free smoke; no npm publish, no GitHub Release, no tag push. Replaces inline `standalone-install-smoke-{linux,windows}` jobs deleted from `ci.yml`. standalone-install-smoke-windows adds step `Windows introspection smoke` (`pwsh`: `npx tsx scripts/windows-introspection-smoke.ts`) — exercises live PowerShell Get-CimInstance paths, asserts no wmic stderr leak. See change: gate-publish-on-smoke-and-tests, replace-wmic-with-powershell. standalone-install-smoke-windows also adds step `Windows boot-parent-liveness smoke` (`pwsh`: `npx tsx scripts/windows-liveness-smoke.ts`) — asserts koffi Tier-2 (`OpenProcess`+`WaitForSingleObject`) loads on real Windows (`bootParentLivenessTier()==="tier2"`), guarding a silent Tier-1 degrade. See change: electron-attach-ownership-fixes. See change: surface-denial-remedy-in-previews — Windows leg runs `scripts/windows-forbidden-subjects-smoke.ts` (host `%SystemRoot%` listed, no POSIX junk). Top-level `permissions: contents: read`. Keeps `cache: pnpm` (read token, no release artifact; design.md D5). See change: harden-ci-cache-poisoning. |
| `ci-electron.yml` | workflow_dispatch only. On-demand Electron build for feature branches. Computes version slug `<base>-ci.<UTC-stamp>.<branch-slug>.<sha7>` from root `package.json#version` + `date -u +%Y%m%d-%H%M%S` + sanitised `GITHUB_REF_NAME` (max 20 chars, `[^a-zA-Z0-9.-]` → `-`, leading/trailing `.-` stripped) + `GITHUB_SHA[:7]`. Validates slug against `publish.yml`'s SemVer regex. Delegates entirely to `_electron-build.yml` with `source_only_bundle: false`, `artifact_retention_days: 14`, `ref: ${{ github.sha }}`. Optional `legs` input forwarded. Prerelease segment SemVer-ranks below `<base>` — `electron-updater` default `allowPrerelease: false` skips these slugs so installed-user update channel is unaffected. No npm publish, no GitHub Release, no tag. Post `fix-ci-electron-runnable-bundles`, CI-dispatched artefacts ship runnable installer layout identical to release builds; earlier `true` value (set by `add-ci-electron-on-demand-build` Decision 3) invalidated by `eliminate-electron-runtime-install` removing every runtime install path; locked by `publish-workflow-contract.test.ts` `ci-electron.yml — runnable-bundle contract` describe block (asserts literal `source_only_bundle: false`). See change: add-ci-electron-on-demand-build, fix-ci-electron-runnable-bundles. Top-level `permissions: contents: read`; passes `use_dependency_cache: true` to `_electron-build.yml` (dev builds, attached to no release). See change: harden-ci-cache-poisoning. |
| `ci-e2e-browser.yml` | Sharded browser-E2E (`tests/e2e/`, 168+ Playwright specs) against the Docker harness. Triggers: `workflow_dispatch` (+ informational `shards` input), `pull_request` on `develop` GATED on the `e2e-browser` label (types `opened`/`synchronize`/`reopened`/`labeled`), and `schedule` cron `'30 7 * * *'` **COMMENTED** (land-dark, like `nightly.yml`; enable at task 5.5 once the baseline is green — exactly one of active/commented, contract-enforced). No `push`. ADVISORY: `continue-on-error` on the PR path only; flipped to required in a separate change after two green nightlies. Jobs: `e2e` (matrix `shard: [1..18]`, `runs-on: ubuntu-latest`, `timeout-minutes: 120`, `fail-fast: false`, `PI_E2E_SEED=1`, `PW_E2E_BOOT_TIMEOUT_MS=1200000` (a cache-less runner spends ~6-8min building the image before the health poll can succeed), `TEST_COPY_MODE=1` (GitHub runners refuse the entrypoint's `mount -t overlay` → the container crash-looped; copy-mode is the spec-sanctioned no-capability fallback, free here because globalSetup boots from an EMPTY workspace), `pnpm exec playwright test --shard=${{ matrix.shard }}/18`, uploads `blob-report-<n>` and `harness-failure-<n>` (globalSetup's container state+log snapshot when the boot never goes healthy — the bundle that made the overlay failure diagnosable), then `if: always()` `Tear down harness` reading the workspace from `test-results/.e2e-managed` **as JSON** (`globalSetup` writes `JSON.stringify`; `require()` of the extensionless file loads it as JS and throws, so the old form's `|| true` left the path empty and the belt-and-braces teardown silently no-op'd on every run) and running `docker/test-down.sh` there — `globalTeardown` is primary, this covers a KILLED Playwright process) + `e2e-browser-relay` (the VARIANT-harness leg: `timeout-minutes: 30`, same label gate + PR-advisory `continue-on-error`, `PI_E2E_SEED=1` + `PI_BROWSER_RELAY_FAKE=1` + `PW_E2E_BOOT_TIMEOUT_MS` + `TEST_COPY_MODE=1`, runs ONLY `tests/e2e/browser-relay.spec.ts`, uploads `blob-report-relay` + the boot-log/failure bundles, `if: always()` teardown. It exists because the relay faucet cannot be a shared default — a live relay occludes the chat for every other spec (S2) — so without this leg the whole file would silently skip in all 18 shards; the spec's own `test.skip` names this job) + `merge-report` (`needs: [e2e, e2e-browser-relay]`, `if: always() && needs.e2e.result != 'skipped'`, `continue-on-error` on the PR path — a non-advisory merge job reds a PR whose shards all died before producing a blob, since `download-artifact` matches nothing — downloads `blob-report-*`, `playwright merge-reports --reporter html` → uploads `playwright-report`). One shard per runner: each shard owns its own harness container, so one shard's death cannot cascade; `test-up.sh` reads the runner's real `MemTotal` and refuses an oversubscribing start, so the runner's RAM figure is not the constraint. `--shard` splits by CONTIGUOUS file order, so one shard can inherit a cluster of slow files: N=6 measured a 38→115 min spread, N=12 still capped shard 6 at 120 min on every dispatch (mcp-*/model-*/notify-*), so N=18 cuts that shard's spec count by a third. No `--global-timeout` — the job's `timeout-minutes` is the whole-run budget (#450). `merge-report` runs on `always() && needs.e2e.result != 'skipped'` (a bare `always()` would red every unlabeled PR) and merges with the LOCKFILE's playwright (`pnpm exec playwright merge-reports`), not a hardcoded version. Locked by `packages/shared/src/__tests__/e2e-browser-workflow-contract.test.ts` (matrix length == every `--shard` denominator, `timeout-minutes` on every job, `if: always()` teardown, merged-report upload, PR advisory). Variant legs `e2e-team` (`playwright.team.config.ts`; `pnpm run build` + team-app build as explicit steps) and `e2e-identity-matrix` (`playwright.identity-matrix.config.ts`): same label gate + PR-advisory `continue-on-error` as the shards, `pi` pinned to the exact version in `docker/Dockerfile`, `timeout-minutes: 30`, own HTML report (`--reporter=list,html`, `if: always()` upload `playwright-report-team` / `playwright-report-identity-matrix`), NOT in `merge-report.needs`. Default `playwright.config.ts` `testIgnore`s both directories. See change: stabilize-browser-e2e (#433 part 2). See change: add-missing-ci-legs (#861). |
| `ci-e2e-electron.yml` | Playwright-Electron suite for native-surface flows (zombie-adoption modal, Doctor version-skew row, tray foreign row) unreachable by web-client E2E + unit tests. **Cadence:** `workflow_dispatch` + `pull_request` path-filter (`packages/electron/**`, `tests/e2e-electron/**`, `playwright.electron.config.ts`, self) — ADVISORY (not a required check). Matrix `os: [ubuntu-latest, windows-latest]`, `fail-fast: false`: ubuntu exercises POSIX `decideIsZombie`, windows the win32 branch (bootParentAlive-only) in the REAL app (task 7.4b). OS-split `npm ci` fallback (bash vs pwsh) → `package` → ubuntu adds `playwright install-deps chromium` + `sysctl apparmor_restrict_unprivileged_userns=0` (noble) then `xvfb-run`; windows runs direct. Harness launches with `--no-sandbox`; Linux binary resolved by forge `executableName` `pi-dashboard` (NOT `chrome-sandbox`). Uploads `playwright-report-electron-<os>`. Second job `job-object-windows` (`continue-on-error`, advisory): downloads bundled Node + package + `npx tsx scripts/windows-job-object-smoke.ts` (spawn-mode Job Object kill-on-close, task 7.4a) — `INFRA` when a lightweight package can't boot the full spawn path (deep validation lives in qa VM smoke). Concurrency cancel-in-progress per ref. See change: electron-attach-ownership-fixes (harness), run-electron-e2e-native-surface (cadence + stabilization). Job `runtime-overlay-e2e` (ubuntu, 90 min; change: electron-runtime-release-pipeline): throwaway Verdaccio (`scripts/runtime-e2e-registry.mjs config` → `VERDACCIO_CONFIG`) → publish `base` → `bundle-server.mjs` with `npm_config_registry` → forge package → publish `good`/`broken` → `playwright … runtime-overlay-update` under xvfb (`PW_ELECTRON_GLOBAL_TIMEOUT_MS` 1 h). Paths filter adds `packages/server/src/runtime-overlay/**` + the two runtime scripts. Top-level `permissions: contents: read` (job-level block on `runtime-overlay-e2e` unchanged). See change: harden-ci-cache-poisoning. Event-keyed cadence (change: add-missing-ci-legs, #531): matrix is `fromJSON` on `github.event_name` — PR → ubuntu+windows, `workflow_dispatch` → +macOS, nightly `schedule` (`17 3 * * *`) → macOS only; `runtime-overlay-e2e` + `job-object-windows` carry `if: github.event_name != 'schedule'`; suite run step `if: runner.os != 'Linux'` (Windows + macOS); no `labeled` trigger; no `continue-on-error` on `electron-e2e`; header names the darwin zombie-adoption quarantine (#531). Locked by `e2e-electron-workflow-contract.test.ts`. |
| `ci-gateway-platform.yml` | `workflow_dispatch` + PR-on-paths (transport sources, the two arms, itself). Runs the gateway's transport contract on the OS that DECIDES it, which one runner structurally cannot see: `posix` matrix (ubuntu + macOS) shims `pi-dashboard` to this checkout and runs `qa/tests/29-gateway-posix-no-tcp.sh` (default start advertises a socket and binds NO TCP port); `windows` runs `qa/tests/28-gateway-windows.ps1` (loopback fallback, no socket artifact, hard-drop reconnect as one session, second-standard-user credential read). Kept OUT of `ci.yml` on purpose — that is a cheap one-runner PR net, this boots real dashboards on three OSes. See change: add-pi-gateway-transport-identity (tasks 5.1/5.5/5.7/12.53/13.8). Top-level `permissions: contents: read`. See change: harden-ci-cache-poisoning. |
| `ci-smoke.yml` | `workflow_dispatch` only. Thin shim around `_smoke.yml` passing `ref: ${{ github.ref }}`. Concurrency `ci-smoke-${{ github.ref }}` cancel-in-progress so re-dispatch on same branch cancels prior run. Use for installer-shaped feature branches (lockfile, `bundle-server.mjs`, native deps, preload-fastify) where PR's cheap `ci.yml` job lacks coverage. Sibling of `ci-electron.yml`. No npm publish, no GitHub Release, no tag push. See change: gate-publish-on-smoke-and-tests. Top-level `permissions: contents: read`. See change: harden-ci-cache-poisoning. |
| `ci.yml` | PR + develop-push net on Node 22, parallel jobs (change: speed-up-ci-affected-tests, D5). Triggers: `push` develop, `pull_request` develop (default types — NO `labeled`, so a label can never publish a fake-green run), `workflow_dispatch` (always full). Top-level `permissions` contents:read. `concurrency` `ci-${{ github.event.pull_request.number \|\| github.sha }}`, `cancel-in-progress` only for PRs. Jobs: `select` (`fetch-depth: 0`, `pnpm install`, `node scripts/select-affected-tests.mjs` — PR `--base origin/<base> --merge-base`, push `--base <before>`, dispatch or `ci:full` label `--full`; uploads artifact `test-selection`; outputs `mode`/`has_real_process`/`ci_scenarios`) · `ci` (guards, spec validate, skill/fixed-tick/settings-path/publish-import checks, PR plugin install-load, kb dist freshness, `lint`, `lint:e2e`, Biome, build + warning gate, post-build publish-imports — NO tests, NO chromium) · `unit` (matrix `shard: [1,2,3,4]` == `SHARD_COUNT`; `assigned.mjs` → empty shard skips steps; chromium; `packages/client/dist/index.html` assertion; `pnpm run test:parallel <files>` or `--shard=n/4` when `enumerated=false`; `verify-executed.mjs`; uploads `vitest-report-unit-<n>` `if: always()`) · `real-process` (`if: has_real_process`; `pnpm run test:real-process <files>`; uploads `vitest-report-real-process`) · `ci-scenarios` (`if: ci_scenarios` — false only for OpenSpec-only diffs; `pnpm run test:ci-scenarios <gated files>`; uploads `vitest-report-ci-scenarios`) · `docker-plugin-load` · `music-pytest` · `ci-result` (`if: always()`, needs EVERY job incl. `select`, runs `check-result.mjs` with `NEEDS_JSON: ${{ toJSON(needs) }}`). Locked by `packages/shared/src/__tests__/ci-affected-selection-contract.test.ts` + `ci-vitest-report-artifact.test.ts`. See changes: isolate-real-process-tests, speed-up-ci-affected-tests. |
| `codeql.yml` | CodeQL advanced setup (replaces default setup). Triggers: push + pull_request on `develop`, weekly schedule. Matrix `actions`/`javascript-typescript`/`python`, `build-mode: none`, `config-file: ./.github/codeql/codeql-config.yml`, analyze category `/language:${{ matrix.language }}` (pinned = default-setup categories, keeps dismissals). Top-level `permissions: contents: read, security-events: write, actions: read`. No cache, no install step. Not a required check. Contract: `packages/shared/src/__tests__/codeql-workflow-contract.test.ts`. See change: add-local-codeql-ratchet. |
| `deploy-site.yml` | Builds + deploys `site/` (hand-written static page; `npm run build` = `node build.mjs` copy + reference check) plus the neutral shell (`packages/shell` → `site/dist/app/`) to GitHub Pages via `actions/deploy-pages@v4`. Triggers: push to `develop` under `site/**` (minus `design-scratch/`), `packages/shell/**`, or the workflow itself; `workflow_dispatch`. **No `release:` trigger** — the default Actions token cannot start runs from release events (never has); the release path is `publish.yml`'s `site-redeploy` job dispatching this workflow on `develop`. Runs `npm run check-release` non-blocking. Locked by `site-deploy-workflow-contract.test.ts` (E9/E10/E12/E14) + `pnpm-migration-contract.test.ts` X6. See change: fix-deploy-site-ship-shell. Build writes `site/dist/deploy-sha.txt` (`${{ github.sha }}`) before upload and runs only on `github.ref == 'refs/heads/develop'` (deploy `needs: build`); daily `site-drift-check.yml` backstops the push trigger. See change: add-missing-ci-legs (#579). |
| `nightly.yml` | Scheduled full-fidelity Verdaccio round-trip (workflow_dispatch active; `cron '0 7 * * *'` present but COMMENTED per land-dark rollout — enable after one clean manual run). Jobs: `resolve` (compute `<base>-nightly.<YYYYMMDD>.<sha7>`, `<base>` = next patch of package.json version) → `verify-publish` (`npm publish --dry-run` all workspaces, cheap pack pre-gate, no network write) → `electron` (`uses: _electron-build.yml` with `registry_url: http://localhost:4873`, `source_only_bundle: false`, `legs: all`, `artifact_retention_days: 7`) → `report` (`if: failure()`, github-script opens/updates a single issue labelled `nightly` naming the failed leg(s) + run URL). ZERO public npm writes / NO Release / NO tag push / NO version-bump commit — locked by `nightly-workflow-contract.test.ts`. See change: add-nightly-verdaccio-build. `electron` passes `use_dependency_cache: true` (nightly artifacts attached to no release). See change: harden-ci-cache-poisoning. |
| `nightly-tests.yml` | Full vitest suite every night (change: speed-up-ci-affected-tests, D6) — the safety net under affected-only selection. Triggers: ACTIVE `cron '0 3 * * *'` + `workflow_dispatch`. Top-level `permissions` contents:read; only `report` adds issues:write + actions:read (least privilege). Jobs: `select` (`--full`) → `unit` ×4 (chromium, dist assertion, `verify-executed`, slow tier included) · `real-process` · `ci-scenarios` (plain `pnpm run test:ci-scenarios`) → `report` (`if: always()`, `fetch-depth: 0`, `scripts/test-selection/nightly-report.mjs`: failing files per job, jobs with NO report, bisect range since the last green SCHEDULED develop run; on a scheduled run opens/updates the single open `nightly-tests` issue or comments+closes it on green; uploads refreshed `test-timings`). Independent of land-dark `nightly.yml`; never publishes/tags. Locked by `packages/shared/src/__tests__/nightly-tests-workflow-contract.test.ts`. |
| `publish.yml` | CI: builds DMG × 2 (macOS arm64 + x64), DEB+AppImage (Linux), NSIS+ZIP+portable (Windows) on native runners;… Top-level `contents: read`; release-privileged jobs install cold (no cache); `electron` passes `use_dependency_cache: false`. → see `publish.yml.AGENTS.md` |
| `site-drift-check.yml` | Daily `schedule` (`23 5 * * *`) + `workflow_dispatch` backstop for `deploy-site.yml`'s single push trigger. Checks out `develop` (full history), runs `node scripts/site-drift-decision.mjs --ref origin/develop`: live `deploy-sha.txt` vs last site-relevant commit; stale → `::warning::` + `gh workflow run deploy-site.yml --ref develop` (skips with `::notice::` while a deploy is queued/in progress or `gh run list` fails; red when the dispatch is rejected). Top `contents: read`, job exactly `contents: read` + `actions: write`, no cache, outside the `pages-deploy` group. Locked by `site-deploy-workflow-contract.test.ts` + `workflow-token-cache-contract.test.ts`. See change: add-missing-ci-legs (#579). |
| `sync-release-version.yml` | Rewrites the download block in `site/index.html` (inline release data — no `latest-release.json` since the static-page rewrite) from `gh api repos/$REPO/releases/latest` via `site/design-scratch/scripts/sync-release.mjs` (assets matched by SHAPE: extension + arch suffix), commits to `develop` as `github-actions[bot]` (a default-token commit does NOT trigger another workflow — the push path filter cannot fire on it). The working release path: `publish.yml` `site-redeploy` dispatches this workflow, waits via correlation-token run lookup + `gh run watch --exit-status` + explicit conclusion check, then dispatches `deploy-site.yml --ref develop` explicitly. Trigger: `workflow_dispatch` ONLY (`correlation` input). `release:` removed — it fired only for hand-published prerelease drafts and that run was incomplete (its GITHUB_TOKEN commit cannot start `deploy-site.yml`). Manual recovery = TWO dispatches: `sync-release-version`, then `deploy-site`. Locked by `site-deploy-workflow-contract.test.ts` E11 + `sync-release-version.yml has no release-event trigger` describe. See change: fix-deploy-site-ship-shell, fix-ci-pipeline-followups. |