From 9ffbae7276e46d2e6105e7a98f1ea49a6bbfe554 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:13:56 +0000 Subject: [PATCH 1/4] feat(security): enable mendys-prod NetworkPolicy in prod + guard against FuzeInfra#501 recurring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that FuzeInfra#501 (authentik-server-ingress port:0 blocking every Argo sync) is fixed by #534, this completes #493: securityService.networkPolicy was never actually applied in prod because syncs were already failing when it landed. Flips it on in values-prod.yaml, mirroring the authentik policy's own base-off/prod-on pattern. Also adds the two guardrails discussed while diagnosing #501: - gate-networkpolicy-ports (helm-validate.yml): kubeconform validates that a NetworkPolicy port is shaped like an IntOrString, not that it's in range. A missing values.yaml default renders as `port: 0`, which type-checks fine and only fails at live API-server admission — exactly how #501 shipped undetected. This step statically rejects any rendered NetworkPolicy port outside 1-65535, no cluster required. - CLAUDE.md: a guideline against "fixing" a kubeconform failure by casting around it (`| int`, `| default`, etc.) instead of restoring the missing value — the specific anti-pattern that masked #501's root cause. Closes #493. --- .github/workflows/helm-validate.yml | 26 ++++++++++++++++++++++++++ CLAUDE.md | 8 ++++++++ deploy/helm/fuzefront/values-prod.yaml | 12 ++++++++++++ 3 files changed, 46 insertions(+) diff --git a/.github/workflows/helm-validate.yml b/.github/workflows/helm-validate.yml index 0e9702f7..23f916d0 100644 --- a/.github/workflows/helm-validate.yml +++ b/.github/workflows/helm-validate.yml @@ -81,3 +81,29 @@ jobs: -ignore-filename-pattern 'authentik-blueprints' \ -summary \ rendered/fuzefront/templates/ + + - name: Guard NetworkPolicy port values (${{ matrix.values }}) + run: | + # kubeconform validates SHAPE (NetworkPolicyPort.port is IntOrString) but + # not RANGE — a missing values.yaml default renders as `port: 0`, which is + # a syntactically valid integer, so kubeconform stays green while the live + # API server rejects it at admission time (ports must be 1-65535). That + # gap is exactly how FuzeInfra#501 shipped: a `| int` cast silently + # coerced an undefined value to 0 to satisfy kubeconform's oneOf schema, + # and every subsequent Argo sync of the whole application failed for days + # before anyone noticed. This step catches numeric out-of-range ports + # (0 or >65535) directly in the rendered manifest, with no cluster + # needed. Named ports (e.g. `port: http`, referencing a container port + # name) are valid and deliberately left unchecked. + fail=0 + for f in rendered/fuzefront/templates/*.yaml; do + grep -q '^kind: NetworkPolicy$' "$f" || continue + while IFS= read -r line; do + val="${line#*: }" + if [[ "$val" =~ ^[0-9]+$ ]] && { [ "$val" -lt 1 ] || [ "$val" -gt 65535 ]; }; then + echo "::error file=$f::NetworkPolicy has invalid port value '$val' (must be 1-65535, got via a rendered default of 0 or similar) — see FuzeInfra#501" + fail=1 + fi + done < <(grep -E '^\s*port:\s*' "$f") + done + exit $fail diff --git a/CLAUDE.md b/CLAUDE.md index ad3d88fb..44455117 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,14 @@ Read the baseline for the full governance model (3 layers, repo tiers, single-re - **Backend:** Express + Postgres, with **Authentik** (identity/SSO) and **Permit** (authorization) for auth. The frontend talks to the API on a **same-origin API base** (no cross-origin base URL) so it works identically under local TLS and prod ingress — never hard-code an absolute API host. - **Runs on FuzeInfra.** Deploys to Kubernetes (kind-fuzeinfra locally / Contabo k3s prod) via Helm. Infra changes are **delegated to FuzeInfra via `@claude`** — never edit FuzeInfra or operate the cluster from here. +## Helm values hygiene — don't cast around a missing default, restore it + +`helm lint` and `kubeconform` are **static schema** checks: they confirm a rendered value has the right *shape*, not that it is semantically valid. This gap shipped a real outage (FuzeInfra#501): a large `values.yaml` restructuring (#523) accidentally dropped `authentik.networkPolicy.port` (and its sibling namespace keys). With the key gone, `{{ $np.port }}` rendered as nil, which correctly failed kubeconform's `oneOf: [integer, string]` schema check for `NetworkPolicyPort.port` — but the fix applied was `{{ $np.port | int }}`, and Sprig's `int` filter silently converts nil to `0`. `0` **is** a valid integer, so kubeconform went green — while the live API server correctly rejects `port: 0` (must be 1–65535) at admission time, which nothing else in CI exercises. Every Argo sync of the whole `fuzefront` Application failed for days before anyone noticed (fixed in #534). + +- **A coercion filter on a `.Values.` lookup (`| int`, `| default X`, `| toString`, …) is a signal to investigate, not a fix for a lint failure.** If kubeconform/helm-lint complains about a missing or wrong-shaped value, find out *why* it's undefined before reaching for a cast. If the field is genuinely optional, declare the default explicitly in `values.yaml` where a reviewer can see it — don't let the template silently absorb a missing value at the render site. `gate-networkpolicy-ports` (`helm-validate.yml`) now catches the specific case of a NetworkPolicy port rendering out of range, but it does not generalize to every field a naked cast could mask. +- **A "fix missing defaults" commit whose diff is dominated by deletions is a restructuring, not an addition** — self-review it accordingly: diff `helm template` output for every values overlay (`values.yaml`, `values-local.yaml`, `values-prod.yaml`) before vs. after, not just the line-level YAML diff, since a reordered/consolidated file makes an eyeballed diff unreliable. +- **Two PRs touching the same top-level `values.yaml` key concurrently is the highest-risk moment for this class of bug** — if your branch has been open a while and merges master while another active PR is landing changes to the same key (e.g. two sibling `networkPolicy` blocks), diff exactly that region post-merge instead of trusting the auto-resolution. + ## Toolchain baseline — Node 24 LTS / React 19 are a floor, not a suggestion These are **minimums every manifest, image, workflow and remote must meet.** FuzeFront is the Module-Federation host, so its React major *is* the shared-singleton contract for the whole family — drift here does not surface as a version warning, it surfaces as a white screen in somebody else's app. diff --git a/deploy/helm/fuzefront/values-prod.yaml b/deploy/helm/fuzefront/values-prod.yaml index 6a362a71..c59d0787 100644 --- a/deploy/helm/fuzefront/values-prod.yaml +++ b/deploy/helm/fuzefront/values-prod.yaml @@ -92,6 +92,18 @@ securityService: googleBrokered: "true" # Prod browser-facing Google callback URL (app host, not auth-dev). googleRedirectUri: "https://app.fuzefront.com/api/v1/security/social/google/callback" + # Ingress NetworkPolicy allowing mendys-prod's datasets-service to reach + # fuzefront-security in-cluster (FuzeFront#493 / FuzeInfra#339). Base + # values.yaml default is OFF; enabling here is the deliberate deploy-window + # step called out in FuzeInfra#501's fix — the policy was never actually + # applied while Argo was stuck on the authentik-server-ingress port:0 bug. + # + # DEPLOY-WINDOW VERIFY after this syncs: from a mendys-prod pod, + # curl -sv --max-time 5 http://fuzefront-security.fuzefront.svc.cluster.local:3002/api/v1/security/session + # should reach the service (not time out / connection-refused), and confirm + # the existing Traefik + intra-fuzefront paths to fuzefront-security still work. + networkPolicy: + enabled: true applicationsService: enabled: true # serves /api/apps (MF app registry) — needed for MF apps to load From 2d929ae8647ad9fd15eea234cd92ece8854c77e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 12:16:17 +0000 Subject: [PATCH 2/4] fix(ci): normalize CLAUDE.md to LF to satisfy gate-line-endings CLAUDE.md had pre-existing CRLF endings from before gate-line-endings existed; touching it in this PR tripped the gate on the file's whole encoding, not just the new content. Renormalizes the file to LF and pins it in .gitattributes so future edits don't retrip this. --- .gitattributes | 5 + CLAUDE.md | 374 ++++++++++++++++++++++++------------------------- 2 files changed, 192 insertions(+), 187 deletions(-) diff --git a/.gitattributes b/.gitattributes index 500b4d7e..7b8cd70b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -18,3 +18,8 @@ deploy/helm/** text eol=lf # regenerated on Linux CI and diffed, so it must be LF too. services/custom-hostname-api/** text eol=lf custom-hostname-client/** text eol=lf + +# CLAUDE.md had somehow accumulated CRLF endings before gate-line-endings +# existed to catch it; pin it to LF so touching it again doesn't retrip the +# gate on the file's pre-existing encoding. +CLAUDE.md text eol=lf diff --git a/CLAUDE.md b/CLAUDE.md index 44455117..747b2382 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,187 +1,187 @@ -# FuzeFront — repo overlay (L1) - -This repo's `CLAUDE.md` **extends** the FuzeSDLC baseline. It does not duplicate it; where this overlay conflicts with the baseline, **this repo wins**, otherwise the baseline governs. - -- **Baseline (L0):** https://github.com/izzywdev/FuzeSDLC/blob/main/CLAUDE.baseline.md (precedence: repo overrides baseline) -- **Tier:** `product` -- **Expert:** `fuzefront-expert` — consult it first on any task to load architecture/deploy/gotcha context (it advises, it does not gate or own deliverables). -- **Manifest:** `.fuze/manifest.json` declares the instantiated agent subset, design-system base, and hardening. - -Read the baseline for the full governance model (3 layers, repo tiers, single-responsibility agents, contract-first fan-out, signed/merged-PR delivery, async orchestration, cross-repo `@claude` delegation). What follows is only the FuzeFront-specific overlay. - -## What FuzeFront is - -- **Module-Federation host shell.** FuzeFront is the host/container application; consuming products and micro-frontends are federated remotes mounted into the shell. Keep the shell's shared-dependency contract (React, the design system) stable — remotes consume it. -- **Backend:** Express + Postgres, with **Authentik** (identity/SSO) and **Permit** (authorization) for auth. The frontend talks to the API on a **same-origin API base** (no cross-origin base URL) so it works identically under local TLS and prod ingress — never hard-code an absolute API host. -- **Runs on FuzeInfra.** Deploys to Kubernetes (kind-fuzeinfra locally / Contabo k3s prod) via Helm. Infra changes are **delegated to FuzeInfra via `@claude`** — never edit FuzeInfra or operate the cluster from here. - -## Helm values hygiene — don't cast around a missing default, restore it - -`helm lint` and `kubeconform` are **static schema** checks: they confirm a rendered value has the right *shape*, not that it is semantically valid. This gap shipped a real outage (FuzeInfra#501): a large `values.yaml` restructuring (#523) accidentally dropped `authentik.networkPolicy.port` (and its sibling namespace keys). With the key gone, `{{ $np.port }}` rendered as nil, which correctly failed kubeconform's `oneOf: [integer, string]` schema check for `NetworkPolicyPort.port` — but the fix applied was `{{ $np.port | int }}`, and Sprig's `int` filter silently converts nil to `0`. `0` **is** a valid integer, so kubeconform went green — while the live API server correctly rejects `port: 0` (must be 1–65535) at admission time, which nothing else in CI exercises. Every Argo sync of the whole `fuzefront` Application failed for days before anyone noticed (fixed in #534). - -- **A coercion filter on a `.Values.` lookup (`| int`, `| default X`, `| toString`, …) is a signal to investigate, not a fix for a lint failure.** If kubeconform/helm-lint complains about a missing or wrong-shaped value, find out *why* it's undefined before reaching for a cast. If the field is genuinely optional, declare the default explicitly in `values.yaml` where a reviewer can see it — don't let the template silently absorb a missing value at the render site. `gate-networkpolicy-ports` (`helm-validate.yml`) now catches the specific case of a NetworkPolicy port rendering out of range, but it does not generalize to every field a naked cast could mask. -- **A "fix missing defaults" commit whose diff is dominated by deletions is a restructuring, not an addition** — self-review it accordingly: diff `helm template` output for every values overlay (`values.yaml`, `values-local.yaml`, `values-prod.yaml`) before vs. after, not just the line-level YAML diff, since a reordered/consolidated file makes an eyeballed diff unreliable. -- **Two PRs touching the same top-level `values.yaml` key concurrently is the highest-risk moment for this class of bug** — if your branch has been open a while and merges master while another active PR is landing changes to the same key (e.g. two sibling `networkPolicy` blocks), diff exactly that region post-merge instead of trusting the auto-resolution. - -## Toolchain baseline — Node 24 LTS / React 19 are a floor, not a suggestion - -These are **minimums every manifest, image, workflow and remote must meet.** FuzeFront is the Module-Federation host, so its React major *is* the shared-singleton contract for the whole family — drift here does not surface as a version warning, it surfaces as a white screen in somebody else's app. - -| Thing | Mandated minimum | Declared in | -|---|---|---| -| Node | `>=24.0.0` (Krypton, Active LTS) | `engines.node` in every manifest; `.nvmrc` = `24` | -| npm | `>=10.0.0` | `engines.npm` | -| `@types/node` | `^24.13.3` | every manifest | -| `react` / `react-dom` | `^19.2.0` | app dependencies | -| React peer range | `^19.0.0` | `peerDependencies` of every published `@fuzefront/*` package + the SDK | -| `@types/react` / `@types/react-dom` | `^19.2.0` | root **and** `design-system` (it ships `.d.ts` that import React types) | -| MF shared `requiredVersion` | `^19.0.0` | host `frontend/vite.config.ts` **and** every remote | -| Docker base image | `node:24-alpine` / `node:24-bookworm-slim` | every Dockerfile | -| CI runner | `node-version: '24.x'` | every workflow | - -- **Raising the floor is fine; lowering it is a breaking change to the family.** Node 18/20/22 and React 18 no longer satisfy these manifests. Bumping the React major means bumping the host, both in-repo remotes, every published peer range and every out-of-repo consumer together — it is never a single-package decision. -- **Host and remote `requiredVersion` must be identical.** If they differ, the remote silently loads its own React copy and dies on "Invalid hook call" at runtime, in the browser, with nothing in CI to catch it. -- **Out-of-repo remotes are bound by this too.** React 18 consumers fail peer resolution against the published packages; see `docs/guides/BUILDING_ON_FUZEFRONT.md`. -- **Nothing enforces this yet.** No CI job reads `engines`, `.nvmrc`, the peer ranges or the MF `requiredVersion` — `gate-version` only checks SemVer bump discipline. Until a `gate-toolchain` exists, this table is the source of truth and the rule survives on review alone. Frozen point-in-time records under `docs/superpowers/plans/`, `sdd/` and `docs/chats/` deliberately still quote the versions current when they were written; they are history, not governance. - -## Design system — FuzeFront IS the base - -- FuzeFront publishes the **"fuse seam" design system** as the base package **`@fuzefront/design-system`** — the single source of truth for color/spacing/type/primitives for the whole Fuze family. -- **Consuming apps extend this base** (add tokens / compose components) in their own repo-local DS package; they **never fork or redefine the primitives**. In this repo the DS package *is* the base (`extendsAs` = `@fuzefront/design-system`). -- `frontend-engineer` is the **sole** editor of `design-system/`. No raw hex/spacing/type in feature code — use the tokens. If a primitive is missing, add it to the base via the design-system skill rather than one-off styling. - -## Hardening / signing — bot-pushed branches MUST be signed (deploy-sensitive) - -This repo enables `required_signatures` on `master`, and **`master` is deploy-on-push**: `release.yml`, `sdk-publish.yml`, and `packages-publish.yml` push **directly to `master`** and trigger deploys/publishes. - -- Those workflows must produce **signed** commits, or `required_signatures` rejects the push. Use one of: - - commit via the **GitHub API / `gh api`** (server-side commits are Verified), or - - run the workflow under an **admin / GitHub App identity** whose commits are signed. -- Human/agent commits are signed via SSH signing (baseline §8 / `governance/hardening-convention.md` §3). Feature-branch commits may be unsigned; the **squash-merge is signed**. -- Because `master` deploys/publishes on push, **never bot-merge here** — merge in a **deploy window** (`hardening.deployOnPush: true`). Hand-deploying to prod is forbidden; prod is GitOps. - -## Feature flags — FuzeFront HOSTS the family flag service - -The family flag standard is **Unleash** (self-hosted OSS) consumed via **OpenFeature** + the private **`@fuzefront/feature-flags`** client (baseline §10). **FuzeFront hosts the Unleash deployment** and owns flag management for the family — consuming repos point their provider at FuzeFront's Unleash with a scoped client token. - -- `feature-flags-engineer` owns the Unleash config + flag taxonomy (`..`) + flag administration here. The Unleash *deploy mechanics* (Helm/Argo/CI on FuzeInfra) are `devops-engineer`; the `@fuzefront/feature-flags` *client package build* is `backend-engineer`. -- `backend-engineer` + `frontend-engineer` plan with flags: wrap new/risky work in a flag **default OFF**, gate **both** server logic and UI, **test both states**, retire stale flags (owner + removal criterion each). A **permission** flag is rollout convenience — real authz stays in **Permit**, never the flag. -- See the `feature-flags` skill (`.claude/skills/feature-flags/`). - -## Android / TWA mobile package - -FuzeFront ships a signed Android APK (Trusted Web Activity) that wraps `https://app.fuzefront.com`. CI handles all building and signing — no manual steps. - -### CI behaviour -- **Any `frontend/**` push to `master`** triggers `build-android-apk.yml`: builds a signed APK and creates a GitHub Release (`android-vN`). -- **Any PR** touching `android/**`, `frontend/public/**`, or `frontend/src/**` also runs the build and uploads a `fuzefront-android-vN` artifact for pre-merge testing — but does **not** publish a Release. -- `workflow_dispatch` is available on the workflow for manual builds with a custom version code. - -### Key identity files — keep in sync -| File | Purpose | -|------|---------| -| `android/twa-manifest.json` | Bubblewrap TWA config (host, colors, SHA-256 fingerprint) | -| `frontend/public/.well-known/assetlinks.json` | Digital Asset Links (same fingerprint as above) | -| `frontend/public/manifest.webmanifest` | Static PWA manifest served during scaffold | -| `frontend/public/icons/pwa-{192,512,maskable-*}.png` | App icons | - -If you rotate the signing keystore or change the key alias, all four files must be updated and `ANDROID_KEYSTORE_B64` / `ANDROID_KEYSTORE_STORE_PASSWORD` / `ANDROID_KEYSTORE_KEY_PASSWORD` GitHub Secrets must be rotated. - -### Agent ownership -- `devops-engineer` — CI/signing pipeline (`android/**`, `build-android-apk.yml`) -- `mobile-frontend-engineer` — responsive shell layout, drawer sidebar, touch targets, PWA/TWA viewport constraints, mobile breakpoints -- `frontend-engineer` — PWA manifest, icons (`frontend/public/`), and design-system non-mobile primitives -- **Never commit `android/keystore/*.keystore`** — gitignored; stored only in `ANDROID_KEYSTORE_B64`. - -## Design-first gate — HTML frames are the source of truth (PenPot is parked) - -**No UI is written before its design is approved.** This closes the SDLC gap that let unverified CSS ship, and — more importantly — that let six fully-built Security backends ship with **no UI at all** and nothing to catch it. Plan of record: `docs/planning/design-first-ui-pipeline.md`. - -**The authoritative design artifact is a set of navigable HTML frames in this repo** — `design/frames//` (`index.html` entry + ordered `01-*.html` screens + `tokens.css` + `manifest.json`), published to GitHub Pages for review. Not PenPot. PenPot is **parked** by owner decision; it complicates the loop without earning it, and a design tool that is not in the repo cannot be gated by CI. Frames are code: they diff, they review, they enforce. - -### Flow -1. **`product-designer`** — the **sole** author of `design/frames/**` and the UX/UI owner — turns the product requirement/user story into frames. **Not `frontend-engineer`**: the implementer must not author the spec it is measured against, exactly as `contract-designer` (not `backend-engineer`) owns the API spec. -2. **Frames are ALWAYS their own PR, and its only content.** CI on it enforces the UX/UI policy: `gate-ds-conformance`, `gate-frames-schema`, `gate-frames-stamped`. -3. The frames declare the **build inventory** (flows / React components / npm packages) — rendered in `index.html`, mirrored in the manifest. Approving the design approves the architecture, so implementation cannot quietly invent a different one. -4. The owner approves **per flow** — one ready flow never waits on an unready sibling. **Reject re-dispatches `product-designer`** for an improving iteration; it does not close the thread. -5. **Merging an approved frames PR is the trigger**: UX QA agents write Playwright specs for each flow that are **ALL RED** first (TDD — the specs fail before an implementation exists), then `frontend-engineer`s build components → flow orchestrators → packages, until the specs go green. -6. `frontend-test-engineer` verifies the built UI against the approved frames. - -### States are contract, not decoration -Frames must show loading, empty, error, and the real fail-closed cases (reveal-once token; remove-last-2FA-factor → 409; demote-the-last-admin; `hasPassword: null` → "set a password first"). **Frames that show only the happy path produce UI that only handles the happy path.** - -### Enforcement — the rule, not the etiquette -`gate-frames-first` fails any PR touching feature UI (`frontend/src/**`, `packages/*-ui/**`) without an approved `design/frames//manifest.json` covering it. Governance nobody can skip beats a step someone is supposed to remember — the whole reason this gate exists is that pushing feature UI with no approved frames was *possible*. - -## UI runtime validation — the console-clean gate - -Design-review checks how the UI *looks*; this gate checks how it *runs*. A UI change that type-checks, passes vitest, and matches its approved frame can still be broken at runtime — an uncaught exception, a 404 on a JS chunk, a **CSP / mixed-content** block under TLS, or a failed **Module-Federation** remote load. None of those surface in unit tests or a static frame diff. - -**Mandate:** no UI work is "done" until it has been rendered in a real Chromium via the **Chrome DevTools MCP** (`chrome-devtools-mcp` plugin, marketplace `chrome-devtools-plugins`) and the **console is clean** — 0 errors, 0 CSP/mixed-content violations, 0 failed app requests, or every remaining message explained. This is a hard gate at every UI hat: - -- **`frontend-engineer` / `mobile-frontend-engineer`** — dev-time **self-check** before reporting `SCOPE DONE` (mobile validates under device emulation). -- **`frontend-test-engineer`** — independent **QA**, on top of the Playwright run, pre- and post-production. A runtime console error is a bug to **REPORT**, never patched by QA. -- **`test-engineer`** (API/service) is excluded — it is browser-less by design. - -The procedure, the FuzeFront gotchas (same-origin API base / no mixed-content under TLS, Module-Federation load), the full MCP capability map (console, network, Lighthouse/perf, a11y, device emulation, heap snapshots), and the DONE-report wording live in the **`ui-runtime-validation`** skill (`.claude/skills/ui-runtime-validation/`). The plugin must be installed in the session (`claude plugin marketplace add ChromeDevTools/chrome-devtools-mcp` → `claude plugin install chrome-devtools-mcp@chrome-devtools-plugins`); it is user/environment-scoped, not committed repo config. - -## Agent worktree lifecycle — reap them, or agents stop launching - -The Agent tool auto-removes an isolated worktree **only if it is unchanged**. Agents exist to change files, so in practice **every productive agent — and every agent killed mid-run** (API error, usage cap, timeout) — leaks its worktree and its `worktree-agent-*` branch. Nothing reaps them by default. - -This is not cosmetic. Each worktree is a full checkout (~2k files, plus `node_modules` if the agent installed). Past **~50** the repo gets slow enough that `git worktree add` exceeds the launcher's timeout and **no agent can start at all** — a self-inflicted DoS. This has already happened here: a fan-out session reached 100+ worktrees and every subsequent launch failed with `Failed to create worktree` until they were reaped. On Windows the leak is worse to clean up — `node_modules` carries read-only attributes, so plain `rm -rf` fails with "Permission denied" and each `git worktree remove` can take minutes. - -- **`scripts/reap-agent-worktrees.sh`** reclaims them. It runs automatically via the **`SessionStart` hook** in `.claude/settings.json`, so a fresh session self-heals; run it by hand any time launches start failing. -- **Safety contract: work is never destroyed.** A worktree is reaped only if it has **no uncommitted changes** AND **no unpushed commits**. Anything dirty or unpushed is reported under `KEPT` and skipped so it can be salvaged. There is deliberately no `--force`. -- `--dry-run` reports without changing anything. -- This is the local counterpart to the branch policy below: `governance-nightly` reaps stale *branches* on the remote; the reaper reaps stale *worktrees* on the developer's disk. Neither covers the other. - -**This is also why the continuous-push rule matters twice over**: an agent that holds work only on local disk can have its worktree reaped-blocked (skipped, cluttering the box) and, if the box is wiped, lose the work entirely. Push early — the reaper only cleans what is safely on origin. - -## Entity identifiers — the owning service mints them, and references carry their type - -Full standard: **`governance/identifier-standard.md`** (enforced by `gate-identifier`). -Design rationale: `docs/planning/entity-identity-and-graph-create.md`. - -Two rules, and one is not enough without the other: - -1. **The service that owns an entity mints its id.** A create body must never accept an `id`/`uuid` for the resource being created, and must set `additionalProperties: false`. A client-chosen id turns a cross-type collision from something an attacker must *find* (probability ~0) into something they *type in* — OWASP API3:2023 BOPLA. Fields naming an entity that already exists (`organizationId`, `userId`) are references, not identity, and are fine. -2. **Every polymorphic reference carries its type**, and no lookup resolves a bare id. §1 alone still loses to an attacker who *learns* an id rather than choosing one. - -**Corollary, always in force: an id is never a capability.** Authorization comes from the token and Permit. "The caller knew the id" is never sufficient. - -**Format is wire-typed, storage-native.** `cus_01h455vb4pex5vsknk084sn02q` on the API (TypeID: prefix + UUIDv7 in base32); a native 16-byte `uuid` column underneath. With services on separate databases there is no shared unique index, so the prefix — checkable offline, with no network call and no cache — is the only defense that always works. Ids are **opaque past the prefix**: never parse further, never assume a length. `mintId()`/`mint_id()` is the only sanctioned constructor. - -**Graph create** uses `lid` in / `idMap` out, with ids minted up front so handlers never learn `lid` existed and reference cycles resolve. A `lid` graph is scoped to **one service's aggregate** — a graph spanning services cannot be created atomically. - -Packages: **`@fuzefront/shared/identity`** (Node) and **`fuzefront-identity`** (Python, `packages/identity-py/`). They are pinned to each other — same prefixes, same codec, same error codes — and `gate_identifier.py --registry-parity` fails CI if they drift, because a mismatch means a reference minted by one language is rejected by the other. - -## Branch lifecycle policy - -Every agent-created branch must reach one of these terminal states — never left open indefinitely: - -| State | Definition | Time limit | -|-------|-----------|-----------| -| **MERGED** | PR squash-merged, branch auto-deleted | — (happy path) | -| **CLOSED** | PR closed (abandoned/superseded), branch auto-deleted | — | -| **ACTIVE** | Commits pushed, PR open, CI running | ≤ 7 days from last commit | -| **PENDING-REVIEW** | PR non-draft, CI green, awaiting owner approval | indefinite while actively reviewed | -| **DRAFT-BLOCKED** | Draft PR labelled `wip`, `hold`, or `blocked` | exempt from staleness | - -`governance-nightly` enforces this daily: closes stale draft PRs (no new commits in 7 days) and deletes branchless branches whose commits are fully reachable from master. - -**Agent branch → auto-merge path — the agent opens its own PR. CI cannot.** - -**Every agent MUST open its own non-draft PR with the `auto-merge` label.** This is not optional and there is no safety net that does it for you. `auto-merge.yml` then calls `gh pr merge --auto --squash --delete-branch`, so the branch self-resolves once all CI gates pass — no human required for routine agent work. - -**CI cannot open a PR here, by design.** `can_approve_pull_request_reviews` is `false` on this repo (`gh api repos/izzywdev/FuzeFront/actions/permissions/workflow`), so `gh pr create` from any workflow fails with *"GitHub Actions is not permitted to create or approve pull requests"*. GitHub bundles create-PR and approve-PR into a single toggle, and `master` is deploy-on-push with required reviews — enabling it would hand every workflow a self-approval path to production. An un-bypassable review gate is worth more than auto-PR convenience. If auto-PR is ever genuinely needed, wire a scoped PAT/GitHub App token rather than flipping the toggle. - -`claude-auto-pr.yml` (workflow name: *Stranded-branch detector*) therefore does **not** create PRs — it detects a branch that has commits but no PR and **fails loudly** so the work gets salvaged rather than silently reaped by `governance-nightly` a week later. - -> **This section previously claimed all four prefixes auto-PR "the moment they are pushed to".** That was false for the life of the workflow: it can never create a PR, and every green run was the early-exit path (*"PR already open"*) because the agent had already opened one. It ran its create path only when actually needed — and failed. A check that passes when its job is already done by someone else, and fails only when asked to work, is not evidence of anything. Assume nothing here is verified because a check is green; verify the deliverable (baseline: *verify the deliverable, not the "finished" claim*). - -Draft PRs are only legitimate when a session explicitly labels them `wip`, `hold`, or `blocked`. - -## Done - -Finish work as a **merged PR**, not local commits — but respect the deploy window above. Every domain agent reports `SCOPE DONE (verified)` + `OUT OF SCOPE — NOT DONE`; only the orchestrator calls a feature complete. +# FuzeFront — repo overlay (L1) + +This repo's `CLAUDE.md` **extends** the FuzeSDLC baseline. It does not duplicate it; where this overlay conflicts with the baseline, **this repo wins**, otherwise the baseline governs. + +- **Baseline (L0):** https://github.com/izzywdev/FuzeSDLC/blob/main/CLAUDE.baseline.md (precedence: repo overrides baseline) +- **Tier:** `product` +- **Expert:** `fuzefront-expert` — consult it first on any task to load architecture/deploy/gotcha context (it advises, it does not gate or own deliverables). +- **Manifest:** `.fuze/manifest.json` declares the instantiated agent subset, design-system base, and hardening. + +Read the baseline for the full governance model (3 layers, repo tiers, single-responsibility agents, contract-first fan-out, signed/merged-PR delivery, async orchestration, cross-repo `@claude` delegation). What follows is only the FuzeFront-specific overlay. + +## What FuzeFront is + +- **Module-Federation host shell.** FuzeFront is the host/container application; consuming products and micro-frontends are federated remotes mounted into the shell. Keep the shell's shared-dependency contract (React, the design system) stable — remotes consume it. +- **Backend:** Express + Postgres, with **Authentik** (identity/SSO) and **Permit** (authorization) for auth. The frontend talks to the API on a **same-origin API base** (no cross-origin base URL) so it works identically under local TLS and prod ingress — never hard-code an absolute API host. +- **Runs on FuzeInfra.** Deploys to Kubernetes (kind-fuzeinfra locally / Contabo k3s prod) via Helm. Infra changes are **delegated to FuzeInfra via `@claude`** — never edit FuzeInfra or operate the cluster from here. + +## Helm values hygiene — don't cast around a missing default, restore it + +`helm lint` and `kubeconform` are **static schema** checks: they confirm a rendered value has the right *shape*, not that it is semantically valid. This gap shipped a real outage (FuzeInfra#501): a large `values.yaml` restructuring (#523) accidentally dropped `authentik.networkPolicy.port` (and its sibling namespace keys). With the key gone, `{{ $np.port }}` rendered as nil, which correctly failed kubeconform's `oneOf: [integer, string]` schema check for `NetworkPolicyPort.port` — but the fix applied was `{{ $np.port | int }}`, and Sprig's `int` filter silently converts nil to `0`. `0` **is** a valid integer, so kubeconform went green — while the live API server correctly rejects `port: 0` (must be 1–65535) at admission time, which nothing else in CI exercises. Every Argo sync of the whole `fuzefront` Application failed for days before anyone noticed (fixed in #534). + +- **A coercion filter on a `.Values.` lookup (`| int`, `| default X`, `| toString`, …) is a signal to investigate, not a fix for a lint failure.** If kubeconform/helm-lint complains about a missing or wrong-shaped value, find out *why* it's undefined before reaching for a cast. If the field is genuinely optional, declare the default explicitly in `values.yaml` where a reviewer can see it — don't let the template silently absorb a missing value at the render site. `gate-networkpolicy-ports` (`helm-validate.yml`) now catches the specific case of a NetworkPolicy port rendering out of range, but it does not generalize to every field a naked cast could mask. +- **A "fix missing defaults" commit whose diff is dominated by deletions is a restructuring, not an addition** — self-review it accordingly: diff `helm template` output for every values overlay (`values.yaml`, `values-local.yaml`, `values-prod.yaml`) before vs. after, not just the line-level YAML diff, since a reordered/consolidated file makes an eyeballed diff unreliable. +- **Two PRs touching the same top-level `values.yaml` key concurrently is the highest-risk moment for this class of bug** — if your branch has been open a while and merges master while another active PR is landing changes to the same key (e.g. two sibling `networkPolicy` blocks), diff exactly that region post-merge instead of trusting the auto-resolution. + +## Toolchain baseline — Node 24 LTS / React 19 are a floor, not a suggestion + +These are **minimums every manifest, image, workflow and remote must meet.** FuzeFront is the Module-Federation host, so its React major *is* the shared-singleton contract for the whole family — drift here does not surface as a version warning, it surfaces as a white screen in somebody else's app. + +| Thing | Mandated minimum | Declared in | +|---|---|---| +| Node | `>=24.0.0` (Krypton, Active LTS) | `engines.node` in every manifest; `.nvmrc` = `24` | +| npm | `>=10.0.0` | `engines.npm` | +| `@types/node` | `^24.13.3` | every manifest | +| `react` / `react-dom` | `^19.2.0` | app dependencies | +| React peer range | `^19.0.0` | `peerDependencies` of every published `@fuzefront/*` package + the SDK | +| `@types/react` / `@types/react-dom` | `^19.2.0` | root **and** `design-system` (it ships `.d.ts` that import React types) | +| MF shared `requiredVersion` | `^19.0.0` | host `frontend/vite.config.ts` **and** every remote | +| Docker base image | `node:24-alpine` / `node:24-bookworm-slim` | every Dockerfile | +| CI runner | `node-version: '24.x'` | every workflow | + +- **Raising the floor is fine; lowering it is a breaking change to the family.** Node 18/20/22 and React 18 no longer satisfy these manifests. Bumping the React major means bumping the host, both in-repo remotes, every published peer range and every out-of-repo consumer together — it is never a single-package decision. +- **Host and remote `requiredVersion` must be identical.** If they differ, the remote silently loads its own React copy and dies on "Invalid hook call" at runtime, in the browser, with nothing in CI to catch it. +- **Out-of-repo remotes are bound by this too.** React 18 consumers fail peer resolution against the published packages; see `docs/guides/BUILDING_ON_FUZEFRONT.md`. +- **Nothing enforces this yet.** No CI job reads `engines`, `.nvmrc`, the peer ranges or the MF `requiredVersion` — `gate-version` only checks SemVer bump discipline. Until a `gate-toolchain` exists, this table is the source of truth and the rule survives on review alone. Frozen point-in-time records under `docs/superpowers/plans/`, `sdd/` and `docs/chats/` deliberately still quote the versions current when they were written; they are history, not governance. + +## Design system — FuzeFront IS the base + +- FuzeFront publishes the **"fuse seam" design system** as the base package **`@fuzefront/design-system`** — the single source of truth for color/spacing/type/primitives for the whole Fuze family. +- **Consuming apps extend this base** (add tokens / compose components) in their own repo-local DS package; they **never fork or redefine the primitives**. In this repo the DS package *is* the base (`extendsAs` = `@fuzefront/design-system`). +- `frontend-engineer` is the **sole** editor of `design-system/`. No raw hex/spacing/type in feature code — use the tokens. If a primitive is missing, add it to the base via the design-system skill rather than one-off styling. + +## Hardening / signing — bot-pushed branches MUST be signed (deploy-sensitive) + +This repo enables `required_signatures` on `master`, and **`master` is deploy-on-push**: `release.yml`, `sdk-publish.yml`, and `packages-publish.yml` push **directly to `master`** and trigger deploys/publishes. + +- Those workflows must produce **signed** commits, or `required_signatures` rejects the push. Use one of: + - commit via the **GitHub API / `gh api`** (server-side commits are Verified), or + - run the workflow under an **admin / GitHub App identity** whose commits are signed. +- Human/agent commits are signed via SSH signing (baseline §8 / `governance/hardening-convention.md` §3). Feature-branch commits may be unsigned; the **squash-merge is signed**. +- Because `master` deploys/publishes on push, **never bot-merge here** — merge in a **deploy window** (`hardening.deployOnPush: true`). Hand-deploying to prod is forbidden; prod is GitOps. + +## Feature flags — FuzeFront HOSTS the family flag service + +The family flag standard is **Unleash** (self-hosted OSS) consumed via **OpenFeature** + the private **`@fuzefront/feature-flags`** client (baseline §10). **FuzeFront hosts the Unleash deployment** and owns flag management for the family — consuming repos point their provider at FuzeFront's Unleash with a scoped client token. + +- `feature-flags-engineer` owns the Unleash config + flag taxonomy (`..`) + flag administration here. The Unleash *deploy mechanics* (Helm/Argo/CI on FuzeInfra) are `devops-engineer`; the `@fuzefront/feature-flags` *client package build* is `backend-engineer`. +- `backend-engineer` + `frontend-engineer` plan with flags: wrap new/risky work in a flag **default OFF**, gate **both** server logic and UI, **test both states**, retire stale flags (owner + removal criterion each). A **permission** flag is rollout convenience — real authz stays in **Permit**, never the flag. +- See the `feature-flags` skill (`.claude/skills/feature-flags/`). + +## Android / TWA mobile package + +FuzeFront ships a signed Android APK (Trusted Web Activity) that wraps `https://app.fuzefront.com`. CI handles all building and signing — no manual steps. + +### CI behaviour +- **Any `frontend/**` push to `master`** triggers `build-android-apk.yml`: builds a signed APK and creates a GitHub Release (`android-vN`). +- **Any PR** touching `android/**`, `frontend/public/**`, or `frontend/src/**` also runs the build and uploads a `fuzefront-android-vN` artifact for pre-merge testing — but does **not** publish a Release. +- `workflow_dispatch` is available on the workflow for manual builds with a custom version code. + +### Key identity files — keep in sync +| File | Purpose | +|------|---------| +| `android/twa-manifest.json` | Bubblewrap TWA config (host, colors, SHA-256 fingerprint) | +| `frontend/public/.well-known/assetlinks.json` | Digital Asset Links (same fingerprint as above) | +| `frontend/public/manifest.webmanifest` | Static PWA manifest served during scaffold | +| `frontend/public/icons/pwa-{192,512,maskable-*}.png` | App icons | + +If you rotate the signing keystore or change the key alias, all four files must be updated and `ANDROID_KEYSTORE_B64` / `ANDROID_KEYSTORE_STORE_PASSWORD` / `ANDROID_KEYSTORE_KEY_PASSWORD` GitHub Secrets must be rotated. + +### Agent ownership +- `devops-engineer` — CI/signing pipeline (`android/**`, `build-android-apk.yml`) +- `mobile-frontend-engineer` — responsive shell layout, drawer sidebar, touch targets, PWA/TWA viewport constraints, mobile breakpoints +- `frontend-engineer` — PWA manifest, icons (`frontend/public/`), and design-system non-mobile primitives +- **Never commit `android/keystore/*.keystore`** — gitignored; stored only in `ANDROID_KEYSTORE_B64`. + +## Design-first gate — HTML frames are the source of truth (PenPot is parked) + +**No UI is written before its design is approved.** This closes the SDLC gap that let unverified CSS ship, and — more importantly — that let six fully-built Security backends ship with **no UI at all** and nothing to catch it. Plan of record: `docs/planning/design-first-ui-pipeline.md`. + +**The authoritative design artifact is a set of navigable HTML frames in this repo** — `design/frames//` (`index.html` entry + ordered `01-*.html` screens + `tokens.css` + `manifest.json`), published to GitHub Pages for review. Not PenPot. PenPot is **parked** by owner decision; it complicates the loop without earning it, and a design tool that is not in the repo cannot be gated by CI. Frames are code: they diff, they review, they enforce. + +### Flow +1. **`product-designer`** — the **sole** author of `design/frames/**` and the UX/UI owner — turns the product requirement/user story into frames. **Not `frontend-engineer`**: the implementer must not author the spec it is measured against, exactly as `contract-designer` (not `backend-engineer`) owns the API spec. +2. **Frames are ALWAYS their own PR, and its only content.** CI on it enforces the UX/UI policy: `gate-ds-conformance`, `gate-frames-schema`, `gate-frames-stamped`. +3. The frames declare the **build inventory** (flows / React components / npm packages) — rendered in `index.html`, mirrored in the manifest. Approving the design approves the architecture, so implementation cannot quietly invent a different one. +4. The owner approves **per flow** — one ready flow never waits on an unready sibling. **Reject re-dispatches `product-designer`** for an improving iteration; it does not close the thread. +5. **Merging an approved frames PR is the trigger**: UX QA agents write Playwright specs for each flow that are **ALL RED** first (TDD — the specs fail before an implementation exists), then `frontend-engineer`s build components → flow orchestrators → packages, until the specs go green. +6. `frontend-test-engineer` verifies the built UI against the approved frames. + +### States are contract, not decoration +Frames must show loading, empty, error, and the real fail-closed cases (reveal-once token; remove-last-2FA-factor → 409; demote-the-last-admin; `hasPassword: null` → "set a password first"). **Frames that show only the happy path produce UI that only handles the happy path.** + +### Enforcement — the rule, not the etiquette +`gate-frames-first` fails any PR touching feature UI (`frontend/src/**`, `packages/*-ui/**`) without an approved `design/frames//manifest.json` covering it. Governance nobody can skip beats a step someone is supposed to remember — the whole reason this gate exists is that pushing feature UI with no approved frames was *possible*. + +## UI runtime validation — the console-clean gate + +Design-review checks how the UI *looks*; this gate checks how it *runs*. A UI change that type-checks, passes vitest, and matches its approved frame can still be broken at runtime — an uncaught exception, a 404 on a JS chunk, a **CSP / mixed-content** block under TLS, or a failed **Module-Federation** remote load. None of those surface in unit tests or a static frame diff. + +**Mandate:** no UI work is "done" until it has been rendered in a real Chromium via the **Chrome DevTools MCP** (`chrome-devtools-mcp` plugin, marketplace `chrome-devtools-plugins`) and the **console is clean** — 0 errors, 0 CSP/mixed-content violations, 0 failed app requests, or every remaining message explained. This is a hard gate at every UI hat: + +- **`frontend-engineer` / `mobile-frontend-engineer`** — dev-time **self-check** before reporting `SCOPE DONE` (mobile validates under device emulation). +- **`frontend-test-engineer`** — independent **QA**, on top of the Playwright run, pre- and post-production. A runtime console error is a bug to **REPORT**, never patched by QA. +- **`test-engineer`** (API/service) is excluded — it is browser-less by design. + +The procedure, the FuzeFront gotchas (same-origin API base / no mixed-content under TLS, Module-Federation load), the full MCP capability map (console, network, Lighthouse/perf, a11y, device emulation, heap snapshots), and the DONE-report wording live in the **`ui-runtime-validation`** skill (`.claude/skills/ui-runtime-validation/`). The plugin must be installed in the session (`claude plugin marketplace add ChromeDevTools/chrome-devtools-mcp` → `claude plugin install chrome-devtools-mcp@chrome-devtools-plugins`); it is user/environment-scoped, not committed repo config. + +## Agent worktree lifecycle — reap them, or agents stop launching + +The Agent tool auto-removes an isolated worktree **only if it is unchanged**. Agents exist to change files, so in practice **every productive agent — and every agent killed mid-run** (API error, usage cap, timeout) — leaks its worktree and its `worktree-agent-*` branch. Nothing reaps them by default. + +This is not cosmetic. Each worktree is a full checkout (~2k files, plus `node_modules` if the agent installed). Past **~50** the repo gets slow enough that `git worktree add` exceeds the launcher's timeout and **no agent can start at all** — a self-inflicted DoS. This has already happened here: a fan-out session reached 100+ worktrees and every subsequent launch failed with `Failed to create worktree` until they were reaped. On Windows the leak is worse to clean up — `node_modules` carries read-only attributes, so plain `rm -rf` fails with "Permission denied" and each `git worktree remove` can take minutes. + +- **`scripts/reap-agent-worktrees.sh`** reclaims them. It runs automatically via the **`SessionStart` hook** in `.claude/settings.json`, so a fresh session self-heals; run it by hand any time launches start failing. +- **Safety contract: work is never destroyed.** A worktree is reaped only if it has **no uncommitted changes** AND **no unpushed commits**. Anything dirty or unpushed is reported under `KEPT` and skipped so it can be salvaged. There is deliberately no `--force`. +- `--dry-run` reports without changing anything. +- This is the local counterpart to the branch policy below: `governance-nightly` reaps stale *branches* on the remote; the reaper reaps stale *worktrees* on the developer's disk. Neither covers the other. + +**This is also why the continuous-push rule matters twice over**: an agent that holds work only on local disk can have its worktree reaped-blocked (skipped, cluttering the box) and, if the box is wiped, lose the work entirely. Push early — the reaper only cleans what is safely on origin. + +## Entity identifiers — the owning service mints them, and references carry their type + +Full standard: **`governance/identifier-standard.md`** (enforced by `gate-identifier`). +Design rationale: `docs/planning/entity-identity-and-graph-create.md`. + +Two rules, and one is not enough without the other: + +1. **The service that owns an entity mints its id.** A create body must never accept an `id`/`uuid` for the resource being created, and must set `additionalProperties: false`. A client-chosen id turns a cross-type collision from something an attacker must *find* (probability ~0) into something they *type in* — OWASP API3:2023 BOPLA. Fields naming an entity that already exists (`organizationId`, `userId`) are references, not identity, and are fine. +2. **Every polymorphic reference carries its type**, and no lookup resolves a bare id. §1 alone still loses to an attacker who *learns* an id rather than choosing one. + +**Corollary, always in force: an id is never a capability.** Authorization comes from the token and Permit. "The caller knew the id" is never sufficient. + +**Format is wire-typed, storage-native.** `cus_01h455vb4pex5vsknk084sn02q` on the API (TypeID: prefix + UUIDv7 in base32); a native 16-byte `uuid` column underneath. With services on separate databases there is no shared unique index, so the prefix — checkable offline, with no network call and no cache — is the only defense that always works. Ids are **opaque past the prefix**: never parse further, never assume a length. `mintId()`/`mint_id()` is the only sanctioned constructor. + +**Graph create** uses `lid` in / `idMap` out, with ids minted up front so handlers never learn `lid` existed and reference cycles resolve. A `lid` graph is scoped to **one service's aggregate** — a graph spanning services cannot be created atomically. + +Packages: **`@fuzefront/shared/identity`** (Node) and **`fuzefront-identity`** (Python, `packages/identity-py/`). They are pinned to each other — same prefixes, same codec, same error codes — and `gate_identifier.py --registry-parity` fails CI if they drift, because a mismatch means a reference minted by one language is rejected by the other. + +## Branch lifecycle policy + +Every agent-created branch must reach one of these terminal states — never left open indefinitely: + +| State | Definition | Time limit | +|-------|-----------|-----------| +| **MERGED** | PR squash-merged, branch auto-deleted | — (happy path) | +| **CLOSED** | PR closed (abandoned/superseded), branch auto-deleted | — | +| **ACTIVE** | Commits pushed, PR open, CI running | ≤ 7 days from last commit | +| **PENDING-REVIEW** | PR non-draft, CI green, awaiting owner approval | indefinite while actively reviewed | +| **DRAFT-BLOCKED** | Draft PR labelled `wip`, `hold`, or `blocked` | exempt from staleness | + +`governance-nightly` enforces this daily: closes stale draft PRs (no new commits in 7 days) and deletes branchless branches whose commits are fully reachable from master. + +**Agent branch → auto-merge path — the agent opens its own PR. CI cannot.** + +**Every agent MUST open its own non-draft PR with the `auto-merge` label.** This is not optional and there is no safety net that does it for you. `auto-merge.yml` then calls `gh pr merge --auto --squash --delete-branch`, so the branch self-resolves once all CI gates pass — no human required for routine agent work. + +**CI cannot open a PR here, by design.** `can_approve_pull_request_reviews` is `false` on this repo (`gh api repos/izzywdev/FuzeFront/actions/permissions/workflow`), so `gh pr create` from any workflow fails with *"GitHub Actions is not permitted to create or approve pull requests"*. GitHub bundles create-PR and approve-PR into a single toggle, and `master` is deploy-on-push with required reviews — enabling it would hand every workflow a self-approval path to production. An un-bypassable review gate is worth more than auto-PR convenience. If auto-PR is ever genuinely needed, wire a scoped PAT/GitHub App token rather than flipping the toggle. + +`claude-auto-pr.yml` (workflow name: *Stranded-branch detector*) therefore does **not** create PRs — it detects a branch that has commits but no PR and **fails loudly** so the work gets salvaged rather than silently reaped by `governance-nightly` a week later. + +> **This section previously claimed all four prefixes auto-PR "the moment they are pushed to".** That was false for the life of the workflow: it can never create a PR, and every green run was the early-exit path (*"PR already open"*) because the agent had already opened one. It ran its create path only when actually needed — and failed. A check that passes when its job is already done by someone else, and fails only when asked to work, is not evidence of anything. Assume nothing here is verified because a check is green; verify the deliverable (baseline: *verify the deliverable, not the "finished" claim*). + +Draft PRs are only legitimate when a session explicitly labels them `wip`, `hold`, or `blocked`. + +## Done + +Finish work as a **merged PR**, not local commits — but respect the deploy window above. Every domain agent reports `SCOPE DONE (verified)` + `OUT OF SCOPE — NOT DONE`; only the orchestrator calls a feature complete. From 6a43b7ba089b33528991edfc414e1d3a5530c90e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:33:50 +0000 Subject: [PATCH 3/4] chore(governance): reconcile managed files to FuzeSDLC v1 [skip ci] --- .claude/agents/backend-engineer.md | 2 ++ .claude/agents/contract-designer.md | 2 +- .claude/agents/test-engineer.md | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.claude/agents/backend-engineer.md b/.claude/agents/backend-engineer.md index a4bcb030..0221a833 100644 --- a/.claude/agents/backend-engineer.md +++ b/.claude/agents/backend-engineer.md @@ -16,6 +16,8 @@ HTTP API + services + business logic + DB schema/migrations + event producers/co **Pagination is mandatory on every unbounded collection endpoint** (baseline §4.1 / `governance/pagination-standard.md`, enforced by `gate-pagination`). Any LIST/collection GET you implement MUST: accept `limit` (apply the contract's default + **enforce the max server-side**, clamping over-max requests) and `cursor` (preferred — opaque, server-issued, encoding sort-key + tiebreaker) or `offset`; return the envelope `{ items, page: { nextCursor|null, hasMore, total? } }`; and walk the full set deterministically (no gaps/dupes under concurrent writes). **Your unit tests assert** the limit clamp, the envelope shape, and that the cursor pages through correctly. An endpoint is exempt only if inherently bounded/singleton and so annotated in the contract (`x-pagination: exempt`). +**Identifiers are server-minted** (baseline §4.2 / `governance/identifier-standard.md`, enforced by `gate-identifier`). Never accept an `id` for a resource you are creating — mint it with `mintId()`/`mint_id()`, the only sanctioned constructor; never call `randomUUID()`/`uuid4()` for an entity id. Validate every incoming reference with `assertRef(type, id)` before use, and key polymorphic lookups on the `(type, id)` pair — never on a bare id. Type repository signatures with the branded `EntityId` so a raw string off `req.body` cannot compile; store via `toUuid()` into a native `uuid` column and render the prefixed form at the serialization boundary. **An id is never a capability** — authorization still comes from the token and the policy engine. Graph create (`lid`/`idMap`) is provided by the shared middleware: mount it and implement nothing per-route. + ## NOT your scope — never implement these (name them for the orchestrator) - **UI / frontend** (incl. any change to `design-system/` — `frontend-engineer` is its sole owner) → that's the `frontend-engineer`. - The **independent acceptance/contract test suite** → that's the `test-engineer` (API/contract) or `frontend-test-engineer` (UI e2e). You write your own unit tests, but you do NOT grade your own feature. diff --git a/.claude/agents/contract-designer.md b/.claude/agents/contract-designer.md index e6c32af5..ccae156f 100644 --- a/.claude/agents/contract-designer.md +++ b/.claude/agents/contract-designer.md @@ -11,7 +11,7 @@ You are the **contract designer** — the **API/event-contract lifecycle owner** ## Your scope (and ONLY this) You are the single owner of the API/event contracts — authoring, **versioning**, linting, and the generated client — not merely their initial design. From the user story / requirements (and the locked product decisions), design, freeze, and thereafter steward: -- the **HTTP API contract** — an OpenAPI/Swagger spec (resources, paths, request/response schemas, error shapes, auth scopes, **pagination per the standard**, **explicit versioning** — bump the spec version on every change and keep a changelog). **Pagination (baseline §4.1 / `governance/pagination-standard.md`, enforced by `gate-pagination`):** every unbounded collection GET in the spec MUST declare `limit` (with default + max) + `cursor` (preferred, opaque) or `offset`, and the `{ items, page: { nextCursor|null, hasMore, total? } }` response envelope; mark a genuinely bounded/singleton endpoint `x-pagination: exempt` (+ `x-pagination-reason`). The contract is the single place these params/envelopes are defined so backend/test/UI all derive from one source; +- the **HTTP API contract** — an OpenAPI/Swagger spec (resources, paths, request/response schemas, error shapes, auth scopes, **pagination per the standard**, **explicit versioning** — bump the spec version on every change and keep a changelog). **Pagination (baseline §4.1 / `governance/pagination-standard.md`, enforced by `gate-pagination`):** every unbounded collection GET in the spec MUST declare `limit` (with default + max) + `cursor` (preferred, opaque) or `offset`, and the `{ items, page: { nextCursor|null, hasMore, total? } }` response envelope; mark a genuinely bounded/singleton endpoint `x-pagination: exempt` (+ `x-pagination-reason`). The contract is the single place these params/envelopes are defined so backend/test/UI all derive from one source; **Identifiers (baseline §4.2 / `governance/identifier-standard.md`, enforced by `gate-identifier`):** a create body MUST NOT declare an `id`/`uuid` for the resource being created and MUST set `additionalProperties: false` — the owning service mints ids; every polymorphic reference (`entityId`, `ownerId`, `subjectId`, …) MUST carry a sibling type discriminator so no lookup resolves a bare id; a create that legitimately needs client-assigned ids is marked `x-client-assigned-id: allowed` (+ `x-client-assigned-id-reason`). Where a client must create linked entities in one request, model it as `lid` in / `idMap` out, scoped to this service's aggregate; - the **event contract** — the Kafka/AsyncAPI **Zod** event schemas + topic names/keys in the shared package, following the topic-prefix convention; - the **generated typed client** — run `openapi-typescript` to emit the `@/-client` package (private `publishConfig` + repository field), so UI, backend, and tests import the SAME types and drift becomes a compile error. **Lint the spec (Spectral)** on every revision, validate the event schemas, **version** the artifacts, regenerate the client, and **open/refresh the contract PR**. That PR — merged/frozen — is the dependency gate for the whole fan-out, and any later contract change re-enters through you, never around you. diff --git a/.claude/agents/test-engineer.md b/.claude/agents/test-engineer.md index 53212bbb..10da6c1e 100644 --- a/.claude/agents/test-engineer.md +++ b/.claude/agents/test-engineer.md @@ -14,6 +14,8 @@ Author the **API/service verification suite against the frozen spec** — contra **Pagination verification (mandatory).** For **every paginated endpoint in the frozen contract** (baseline §4.1 / `governance/pagination-standard.md`), your suite independently asserts: the endpoint accepts `limit` + `cursor|offset`; the response matches the `{ items, page: { nextCursor|null, hasMore, total? } }` envelope; **`limit` is enforced** (a request over the declared max is clamped, never returns more); and **the cursor walks the whole set** — paging with the returned `nextCursor` visits every item exactly once with no gaps/dupes and terminates (`nextCursor: null` / `hasMore: false`) at the end. An endpoint marked `x-pagination: exempt` is skipped (and you confirm it is genuinely bounded/singleton). +**Identifier verification (mandatory).** For every create in the frozen contract (baseline §4.2 / `governance/identifier-standard.md`), your suite independently asserts: a body carrying an `id` is **rejected** (422), not silently accepted or echoed; an id minted for one entity type is **rejected** where another type is expected (the cross-type confusion this standard exists to stop); a polymorphic reference without its type discriminator is rejected; and — the case implementers most often miss — that **knowing an id grants nothing**: a caller authorized for entity A presenting a valid id for entity B is denied. Where graph create is used, assert `idMap` covers every first-class entity created and that a `lid` naming an entity this service does not own is rejected. + ## File bugs in Jira when a test reveals a real defect A failing test against a real bug is a *valuable deliverable* — but the deliverable isn't just the red test, it's a **tracked ticket**. When your suite uncovers a genuine product defect, **file a bug in Jira** through `agile-manager`'s ticket standards: use the `ticket-creator` skill's **bug template** (and the Atlassian MCP) to create a well-formed bug — repro steps, expected vs actual, the failing test that proves it, severity, and a link back to the contract/acceptance criterion it violates. This routes the defect to the implementer (`backend-engineer` / `frontend-engineer`) instead of silently fixing it yourself. Keep the failing test in the suite so the bug stays provable until closed. From 5e3c8b0793a2f1b890e27c3f9fa0952571b84539 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 20:35:46 +0000 Subject: [PATCH 4/4] chore: retrigger CI after governance-sync bot push