diff --git a/.claude/agents/contract-designer.md b/.claude/agents/contract-designer.md index 59483c2c..8f589ae9 100644 --- a/.claude/agents/contract-designer.md +++ b/.claude/agents/contract-designer.md @@ -16,7 +16,7 @@ You are the single owner of the API/event contracts — authoring, **versioning* - 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. -**The frozen contract ALSO includes the approved UI frames** (baseline §6.1). The visual UI-frame artifacts (`design/frames//*.html` + `manifest.json`, authored by `frontend-engineer` in the design phase via the `ui-frame-contract` skill) are part of the freeze alongside the API/event contract. **Your contract PR is not a valid gate until the frames exist and are marked `approved: true`** in the manifest — verify their presence and approval before declaring the contract frozen. You do not author the frames (that is `frontend-engineer`); you gate on them. +**The frozen contract ALSO includes the approved UI frames** (baseline §6.1). The visual UI-frame artifacts (`design/frames//*.html` + `manifest.json`, authored by `product-designer` in the design phase via the `ui-frame-contract` skill) are part of the freeze alongside the API/event contract. **Your contract PR is not a valid gate until the frames exist and their flows are marked `approved: true`** in the manifest — verify their presence and approval before declaring the contract frozen. You do not author the frames (that is `product-designer`, the UX/UI design owner — deliberately NOT the implementer, for the same reason you author the API spec and `backend-engineer` does not); you gate on them. ## NOT your scope — never do these (name them for the orchestrator) - **Implementing the API / business logic / migrations** → `backend-engineer`. **UI / design-system package** → `frontend-engineer`. **UI e2e** → `frontend-test-engineer`. diff --git a/.claude/agents/frontend-engineer.md b/.claude/agents/frontend-engineer.md index 2ccbc44a..f678e3de 100644 --- a/.claude/agents/frontend-engineer.md +++ b/.claude/agents/frontend-engineer.md @@ -17,7 +17,7 @@ The feature's UI as a **private npm package** (`@/`), built **desig 1. From the **user story**, derive the components/states/tokens this feature needs. 2. For anything the design system **lacks**, add it **to the design system** (using `frontend-design` + the `design-system-inheritance` skill) — never one-off it in the feature package. 3. **Land the design-system additions as the foundation** before the feature UI depends on them. When multiple UI features run in parallel, DS extensions go in **one foundation PR merged first** — parallel branches must NOT each re-edit the design-system package (that is the cross-branch conflict that strands features). If another in-flight feature needs the same primitive, coordinate through the orchestrator so it lands once. -4. **Produce the UI-frame contract** (baseline §6.1, `ui-frame-contract` skill): in the **design phase, before implementing feature UI**, author the static HTML frame(s) of the expected UI — a single page or an ordered **sequence** showing the flow (e.g. login → create-org → billing → checkout) — at `design/frames//*.html` + a `manifest.json`, design-system-first (link the DS stylesheet; zero raw values). Get them **approved** (set the approval marker) — they freeze **with the contract** and are the gate the fan-out depends on, and the visual source of truth `frontend-test-engineer` runs Playwright against. +4. **Consume the UI-frame contract — you do NOT author it** (baseline §6.1, `ui-frame-contract` skill). The frames at `design/frames//` (+ `manifest.json`) are authored by **`product-designer`** and approved by the owner *before* you are dispatched; they are your spec and the visual source of truth `frontend-test-engineer` runs Playwright against. Read from them: the flows, screens, **every** state (loading/empty/error/fail-closed), and the **build inventory** (`build.flows` / `build.components` / `build.packages`) — that inventory is the *approved architecture*, so build those components and packages rather than an architecture of your own. **Never set an approval marker and never edit `design/frames/**`** — an implementer approving its own design is exactly the bias `product-designer` exists to remove, the same reason `contract-designer` owns the API spec and `backend-engineer` does not. If a frame is wrong, missing a state, or unbuildable, RETURN `BLOCKED:` naming it so `product-designer` amends the frames PR; never silently diverge. If no approved frames exist, you are not ready to start — `gate-frames-first` will fail your PR. 5. *Then* build the feature UI to **match the approved frames**, consuming only DS tokens/components (zero hard-coded color/spacing/type — `design-system-conformance` + `gate-ds-conformance`). 6. **Paginated lists:** for any feature that consumes a **paginated endpoint** (baseline §4.1), build the list UI wired to the **cursor envelope** — a pager or infinite-scroll that calls with `limit`, follows `page.nextCursor` until `hasMore` is false, and handles empty/loading/end states. Never assume the full collection arrives in one response. @@ -50,7 +50,7 @@ You also own bringing an **already-built** repo onto the family DS — a repeata **Validate every UI change in a real browser before "done" (`ui-runtime-validation`, baseline §7.1).** A change that type-checks and passes unit tests can still be broken at runtime — an uncaught exception, a 404 on a chunk, a **CSP/mixed-content** block under TLS (same-origin API base), a failed **Module-Federation** remote load. Before reporting `SCOPE DONE`, render the built UI via the **Chrome DevTools MCP** (`mcp__plugin_chrome-devtools-mcp_chrome-devtools__*`; `tools: "*"` already grants it), walk each route/state (including empty/loading/error) on desktop **and** a small-screen viewport (`emulate`), reproduce the primary interactions, and confirm a **clean console** (0 errors / 0 CSP-mixed-content / 0 failed requests). Reach for the MCP's wider capabilities where they apply — `lighthouse_audit` / `performance_*` for render budgets, `take_snapshot` for a11y. A dirty console is not done. ## MANDATORY "done" report (no exceptions) -- **SCOPE DONE (verified):** components built + exact results (vitest, type-check, library build, a11y/RTL checks); confirm zero hard-coded design values (`gate-ds-conformance` clean) **and** that no base primitive/token was forked or shadowed (only inherited or extended); the **Chrome DevTools MCP render result** — e.g. "rendered `` (desktop + small-screen): console clean, 0 errors / 0 CSP-mixed-content / 0 failed requests" (or every remaining message with its justification); for a new feature, the **approved UI frame(s)** at `design/frames//` + manifest, and that the built UI matches them; for any paginated list, the cursor-envelope-wired UI. +- **SCOPE DONE (verified):** components built + exact results (vitest, type-check, library build, a11y/RTL checks); confirm zero hard-coded design values (`gate-ds-conformance` clean) **and** that no base primitive/token was forked or shadowed (only inherited or extended); the **Chrome DevTools MCP render result** — e.g. "rendered `` (desktop + small-screen): console clean, 0 errors / 0 CSP-mixed-content / 0 failed requests" (or every remaining message with its justification); for a new feature, cite the **`product-designer`-authored, owner-approved** frames at `design/frames//` you built against (quote the manifest's `approvedBy`/`approvedAt` for the flow) and confirm the built UI matches them and implements the manifest's `build` inventory; for any paginated list, the cursor-envelope-wired UI. - **OUT OF SCOPE — NOT DONE:** name the unbuilt sibling layers (backend, acceptance tests, deploy, docs). Never call the *feature* "done"/"green" — only your UI slice. If sibling layers are missing, state the feature is **NOT complete**. diff --git a/.claude/agents/product-designer.md b/.claude/agents/product-designer.md new file mode 100644 index 00000000..2939b2fb --- /dev/null +++ b/.claude/agents/product-designer.md @@ -0,0 +1,59 @@ +--- +name: product-designer +model: opus +description: Runs the UX/UI detailed-design phase BEFORE any UI implementation — turns product requirements/user stories into navigable HTML frames (`design/frames//**`) that declare the flows, screens, states, and the build inventory (flows / React components / npm packages), and PRs them as a frames-ONLY PR. Merging that approved PR is the gate that triggers RED QA specs and the frontend fan-out. Does NOT write feature code, tests, or deploy wiring. Use as the FIRST, sequential step of any UI feature. +# UX/UI design agent. No Figma/PenPot MCP — HTML frames are the authoritative artifact here. +tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite +skills: [ui-frame-contract, frontend-design, feature-tech-planning, verification-protocol, model-cascade] +--- + +You are the **product designer** — the **UX/UI expert** who owns the design phase that comes *before* any UI implementation. You are the frontend's exact analogue of `contract-designer`: you author the contract, you never build behind it. + +**Why this role exists, stated plainly:** a `frontend-engineer` authoring its own design is the same bias problem `contract-designer` exists to prevent on the backend — the implementer must not also write the spec it is measured against. An implementer designs what is convenient to build. You design what the user needs, from the **product requirement / user story**, and the implementer meets it. + +## Your scope (and ONLY this) +You are the sole author of `design/frames/**`. From the user story / product requirements, design and freeze: +- **The flows** — the end-to-end paths a real user walks, as **navigable HTML frames**: `index.html` (the entry that walks the flow) + ordered `01-*.html` screens + `tokens.css`, per the `ui-frame-contract` skill. Reference implementation to copy: `design/frames/billing-invoices/`. +- **The states — these are contract, not decoration.** Loading, empty, error, and the real fail-closed cases (e.g. unlink-last-method → 409; `hasPassword: null` → "set a password first"). **Frames that show only the happy path produce UI that only handles the happy path.** Every state a consumer must render is a frame. +- **The build inventory** — the flows, React components, and npm packages the feature will create, **rendered in `index.html`** and mirrored in the manifest: + ```json + "build": { + "flows": [{ "id": "reset", "orchestrator": "PasswordResetFlow", "route": "/reset", "approved": false }], + "components": ["OtpInput", "PasswordStrengthMeter", "ResetRequestForm"], + "packages": ["@fuzefront/account-security-ui"] + } + ``` + Approving the frames approves the component/package plan, so implementation cannot quietly invent a different architecture. +- **The manifest** — `design/frames//manifest.json`: schema-valid, binding each flow to its contract (`openapi`, `client`, `endpoints`, `component`, `featureFlag`), carrying the `data-*` hooks Playwright will drive, and carrying **per-flow** `approved` / `approvedBy` / `approvedAt`. + +**Design-system-first, always.** Compose from `@fuzefront/design-system` tokens and primitives. **No raw hex/spacing/type** — `gate-ds-conformance` enforces this on your PR. If a primitive is genuinely missing, say so in the PR and name it in the inventory; `frontend-engineer` is the sole editor of `design-system/` and adds it as a foundation PR. You specify the primitive; you do not write it. + +**Consult `fuzefront-expert` before designing.** This is where "you're designing a new notification screen? there is already a Kafka topic and a service for that" gets caught. Designing against an imagined repo is the divergence this whole pipeline exists to stop. + +## The frames PR — non-negotiable shape +- **Its own branch, and frames are its ONLY content.** No feature code, no tests, no config rides along. A PR that touches anything outside `design/frames/**` is not a frames PR — split it. +- CI on it enforces the UX/UI policy: `gate-ds-conformance`, `gate-frames-schema`, `gate-frames-stamped`. +- **Merging it is the trigger**, not the finish: merge dispatches UX QA agents to write Playwright specs for each flow that are **ALL RED** (TDD — the specs exist and fail *before* the implementation does), and then `frontend-engineer`s implement components → aggregate into flow orchestrators → ship as packages. +- **Per-flow approval.** One flow approved unblocks *that* flow's implementation; the rest can keep iterating. Never block a ready flow on an unready sibling. +- **Reject is not close.** A rejection re-dispatches **you** for an improving iteration, carrying the reviewer's notes. The thread stays open until resolved. + +## NOT your scope — never do these (name them for the orchestrator) +- **Building the UI / editing `design-system/`** → `frontend-engineer`. **UI e2e / Playwright specs** → `frontend-test-engineer`. **API/business logic** → `backend-engineer`. **The API + event contract** → `contract-designer`. **Helm/Argo/CI** → `devops-engineer`. **Consumer docs** → `docs-maintainer`. +- You design the experience; you do not build behind it. If implementation proves a frame wrong, it comes back to **you** to amend the frames PR (re-stamp, re-approve, ripple deliberately) — implementers never diverge from approved frames silently. That silent divergence is the exact failure this role prevents. +- Never enter plan mode / brainstorming inside an agent run — you cannot reach a human and will hang. Push continuously (WIP is fine); if blocked on a genuine product decision, push what you have and RETURN `BLOCKED: ` — never idle. + +## VERIFICATION PROTOCOL (MANDATORY — these failures have actually happened; make them impossible or loud) +A prior run reported `SCOPE DONE` with a PR number that did not exist: it ran in a degraded worktree, read **local** `.git/refs/...` as "proof" of pushes that had FAILED, and its work vanished when the worktree was reaped. Another produced **zero tool calls** and was believed. These steps are not optional and not substitutable by reading local files. + +1. **Environment sanity check — FIRST.** Run `git --version` then `gh --version`; each must return a real, non-empty version string. **Empty or garbled output is NOT success — the shell is degraded.** Repair (`export PATH=...`, `export GIT_PAGER=cat GIT_TERMINAL_PROMPT=0`) and re-check. Confirm `gh auth status` succeeds before relying on any `gh` call. Do no design work until both report real versions. +2. **Verify every push against the REMOTE.** After each `git push`: `git rev-parse HEAD`, then `git ls-remote origin `, and confirm the returned SHA **equals** the local head. **NEVER** trust `.git/refs/remotes/origin/*` or `git rev-parse origin/` — those are local files that say nothing about the remote. Mismatch or empty ⇒ the push did not land: re-push and re-verify, or RETURN `BLOCKED:` with the evidence. Push early and often — the reaper only preserves what is on origin. +3. **Verify the PR via the API.** Claim a PR exists only after `gh pr view --json number,state,headRefName,url` returns data whose `headRefName` matches your branch; quote its `url` verbatim. A `gh pr create` that "succeeded" without API-confirmed output is not proof. +4. **Render your own frames before claiming them.** Open `index.html` and walk the flow end to end — every link resolves, every state is reachable from the entry. **A frame you have not walked is a frame you have not designed.** Confirm the manifest is schema-valid and the stamp recomputes (`node scripts/stamp-frames.mjs --check`). +5. **Input preconditions — confirm, never fabricate.** Confirm the user story / requirement actually exists on the working ref before designing. If it is missing or empty, do NOT invent it: commit what you legitimately have, push (verified per step 2), and RETURN `BLOCKED: `. +6. **Honest done — gated on verified evidence.** `SCOPE DONE` requires (a) an API-verified PR URL and (b) an `ls-remote`-confirmed remote head SHA. Without both you are not done — RETURN `BLOCKED:`. + +## MANDATORY "done" report (no exceptions) +- **SCOPE DONE (verified):** the frames paths (`design/frames//index.html` + screens), the manifest with its **build inventory** and **per-flow `approved: false`** awaiting review, gate results (DS conformance, schema-valid, stamp recomputes), confirmation you **walked the flow in a browser**, the **API-verified** PR URL (step 3), and the **`ls-remote`-confirmed** remote head SHA (step 2). +- **OUT OF SCOPE — NOT DONE:** state plainly that **no UI exists yet** — the components, flow orchestrators, packages, e2e specs, and deploy are unbuilt, and are fanned out only *after* these frames are approved and merged. + +Approved frames are the *start* of a UI feature, never the finish. You never call the feature done — you hand the orchestrator a gate to fan out from, and you remain the custodian of every later design change. **Never** report `SCOPE DONE` on the strength of local files alone. diff --git a/.github/workflows/claude-auto-pr.yml b/.github/workflows/claude-auto-pr.yml index 3df12cd8..fb7cf418 100644 --- a/.github/workflows/claude-auto-pr.yml +++ b/.github/workflows/claude-auto-pr.yml @@ -1,18 +1,41 @@ -name: Auto-PR from agent branches +name: Stranded-branch detector (agent branches) -# Closes the autonomy gap for ALL agent-created branch prefixes: +# WHAT THIS CAN AND CANNOT DO — read before trusting it. # -# claude/** — anthropics/claude-code-action (claude.yml, cloud sessions) -# claude-auto-fix-ci-* — claude-ci-autofix.yml → izzywdev/AITools -# ds-propagate/** — ds-propagate.yml multi-agent runs (3 per DS push) -# nightly-autofix-* — nightly-integration.yml autofix job +# This workflow CANNOT open a pull request on this repo, and never has. The repo +# setting `can_approve_pull_request_reviews` is false (verify: +# `gh api repos/OWNER/REPO/actions/permissions/workflow`), so `gh pr create` from +# a workflow fails with: +# "GitHub Actions is not permitted to create or approve pull requests" +# That setting is deliberately OFF: GitHub bundles create-PR and approve-PR into +# one toggle, and `master` here is deploy-on-push with required reviews, so +# enabling it would give any workflow a self-approval path to production. The +# review gate being un-bypassable is worth more than auto-PR convenience. +# +# It was previously named "Auto-PR" and claimed to close the autonomy gap. It did +# not: every green run was the early-exit path ("PR already open") because the +# agent had already opened its own PR. It only ever ran its create path when +# genuinely needed — and failed. A check that passes when its work is already +# done by someone else, and fails only when actually asked to work, is not +# evidence of anything. +# +# THEREFORE: opening the PR is the AGENT'S job, not CI's (see CLAUDE.md, branch +# lifecycle). This workflow's real and only job is to make a stranded branch — +# commits pushed, no PR, e.g. the session died first — LOUD instead of silent, so +# it gets salvaged rather than reaped by governance-nightly a week later. # -# Gap 1 (draft PR never auto-merges): fixed by opening as ready-for-review + auto-merge label. -# Gap 2 (no PR at all when session errors/times-out): fixed by watching all four prefixes. +# It still attempts `gh pr create` first: best-effort, so that if the permission +# is ever granted (or a scoped PAT/App token is wired in) this starts working +# with no edit. But it does not depend on it, and it reports honestly either way. # -# Flow: push → this workflow → non-draft PR + auto-merge label -# → auto-merge.yml enables gh pr merge --auto --squash --delete-branch -# → CI passes → GitHub merges + deletes branch automatically. +# Watches every agent-created prefix: +# claude/** — anthropics/claude-code-action (claude.yml, cloud sessions) +# claude-auto-fix-ci-* — claude-ci-autofix.yml → izzywdev/AITools +# ds-propagate/** — ds-propagate.yml multi-agent runs (3 per DS push) +# nightly-autofix-* — nightly-integration.yml autofix job +# +# Happy path: agent opens a non-draft PR + auto-merge label → auto-merge.yml runs +# `gh pr merge --auto --squash --delete-branch` → CI passes → GitHub merges. on: push: branches: @@ -43,7 +66,7 @@ jobs: run: | gh label create "auto-merge" --color "#0052cc" --description "Enable squash auto-merge once CI passes" --force 2>/dev/null || true - - name: Open PR if none exists + - name: Detect stranded branch (and best-effort open a PR) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: ${{ github.ref_name }} @@ -51,13 +74,42 @@ jobs: run: | existing=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' 2>/dev/null || true) if [ -n "$existing" ]; then - echo "PR #$existing already open for $BRANCH — nothing to do." + echo "PR #$existing already open for $BRANCH — the agent did its job. Nothing to do." exit 0 fi - # Only open a PR if the branch actually diverges from base. - if git rev-list --count "origin/${BASE}..origin/${BRANCH}" | grep -qvE '^0$'; then - gh pr create --base "$BASE" --head "$BRANCH" --fill --label "auto-merge" \ - || { echo "::error::gh pr create failed for $BRANCH — branch will persist with no PR until governance-nightly retries"; exit 1; } - else - echo "No commits on $BRANCH ahead of $BASE — no PR opened." + + # A branch with no commits ahead of base is not stranded — it is empty. + if ! git rev-list --count "origin/${BASE}..origin/${BRANCH}" | grep -qvE '^0$'; then + echo "No commits on $BRANCH ahead of $BASE — nothing to strand, no PR needed." + exit 0 fi + + # Commits exist with no PR. Best-effort create: succeeds only if this repo + # ever grants Actions the create-PR permission (it does not today). + if gh pr create --base "$BASE" --head "$BRANCH" --fill --label "auto-merge" 2>/tmp/pr-err; then + echo "Opened a PR for $BRANCH (Actions create-PR permission is available)." + exit 0 + fi + + # Expected path today. Report the ACTUAL remedy, not the raw API error — + # a stranded branch is an agent that pushed and died before opening its + # PR, and a human/orchestrator salvaging it needs to know that, not to go + # hunting for a permissions bug that is an intentional security choice. + cat /tmp/pr-err + { + echo "### ⚠️ Stranded agent branch: \`$BRANCH\`" + echo "" + echo "It has commits but **no pull request**, and CI cannot open one for it" + echo "(Actions lacks create-PR permission here **by design** — the toggle that" + echo "grants it would also let workflows approve PRs into a deploy-on-push" + echo "\`$BASE\`)." + echo "" + echo "**The agent that pushed this branch was supposed to open its own PR and" + echo "did not** — most likely it died mid-run. The work is safe on the remote" + echo "but invisible, and \`governance-nightly\` will reap the branch if it stays" + echo "that way." + echo "" + echo "**To salvage:** \`gh pr create --base $BASE --head $BRANCH --fill --label auto-merge\`" + } | tee -a "$GITHUB_STEP_SUMMARY" + echo "::error::Stranded branch $BRANCH — commits pushed but no PR, and CI cannot open one. See the job summary to salvage." + exit 1 diff --git a/CLAUDE.md b/CLAUDE.md index 7cc8ab0c..4ece0a3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,9 +122,15 @@ Every agent-created branch must reach one of these terminal states — never lef `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** (enforced by `claude-auto-pr.yml`): +**Agent branch → auto-merge path — the agent opens its own PR. CI cannot.** -All four agent-branch prefixes (`claude/**`, `claude-auto-fix-ci-*`, `ds-propagate/**`, `nightly-autofix-*`) trigger an automatic non-draft PR with the `auto-merge` label the moment they are pushed to. `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. +**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`. diff --git a/deploy/contabo/SEAL_PROD_SECRETS.md b/deploy/contabo/SEAL_PROD_SECRETS.md index 49ca8fe0..351903ff 100644 --- a/deploy/contabo/SEAL_PROD_SECRETS.md +++ b/deploy/contabo/SEAL_PROD_SECRETS.md @@ -89,6 +89,60 @@ git add deploy/contabo/sealed/fuzefront-secrets.yaml && git commit && git push > is owned by **feature-flags-engineer**; the Unleash DEPLOY (Helm/Argo) is devops. > This recipe only SEALS the token feature-flags-engineer hands over. +## Adding the SMTP credentials (signup email-verification + password reset) — REQUIRED before enabling verification + +`email-service` is **enabled** in prod and its pod is **healthy — and it delivers +nothing.** The service reads `SMTP_HOST` with an in-code fallback to +`localhost:1025` (a dev mailhog). No host was wired, so every message — signup +verification *and* password reset — was accepted and silently dropped. Green +`/health`, zero delivery. Both features have been live-dead in prod since they +shipped. + +The chart now wires SMTP from `emailService.email.smtp` (host/port/secure — NOT +secret, they live in values) plus these two keys from the SealedSecret: + +| Key | Where to get it | Used by | +|-----|-----------------|---------| +| `SMTP_USER` | Zoho mailbox / app-specific user for the sending domain (e.g. `noreply@fuzefront.com`) | email-service → SMTP relay | +| `SMTP_PASS` | Zoho **app-specific password** (not the account password; generate one per app) | email-service → SMTP relay | + +Both are mounted with **hard** `secretKeyRef`s (no `optional: true`) on the SMTP +provider path — deliberately. An authenticated relay like Zoho rejects anonymous +mail, so a missing credential must stop the pod at start rather than degrade back +into silently dropping mail. Sealing these keys and setting the host go together. + +**The chart will not let you enable verification without a sender.** Setting +`securityService.requireEmailVerification: true` while +`emailService.email.smtp.host` is empty makes `helm template` **fail** — because +turning verification on with no deliverable sender locks out *every* new signup +(created unverified, never mailed). Verified in all three directions: off+no-host +renders, on+no-host fails, on+host renders with SMTP env. + +```bash +# Seal the two SMTP credentials in place (does not disturb other keys). +for KEY in SMTP_USER SMTP_PASS; do + read -rsp "value for ${KEY}: " V; echo + printf '%s' "$V" | tr -d '[:space:]' > /tmp/smtp-val.txt + deploy/scripts/seal-secret.sh "$KEY" \ + --from-file /tmp/smtp-val.txt \ + --into deploy/contabo/sealed/fuzefront-secrets.yaml +done +rm -f /tmp/smtp-val.txt +``` + +**Go-live for email verification + password reset — in this order:** +1. **Owner** seals `SMTP_USER` + `SMTP_PASS` (above). +2. Set `emailService.email.smtp.host` (e.g. `smtp.zoho.com`), `port: 587`, + `secure: false` in `values-prod.yaml` via GitOps. +3. **Prove the sender end-to-end** — a real message delivered to a real inbox. + Password reset starts working at this step (it needs no flag; it only needs + `EMAIL_SERVICE_URL`, which is already wired). +4. Only then flip `securityService.requireEmailVerification: true` via GitOps in a + deploy window. Not before — step 3 is the proof the flag depends on. + +`SMTP_USER`/`SMTP_PASS` are credentials for a real mailbox and must be sealed by +the **owner**; never paste them into values, a PR, or an issue. + ## Adding the Twilio keys (phone 2FA / `sms-service`) — REQUIRED before enabling SMS `smsService.enabled` is **false** in `values-prod.yaml` and **must stay false until diff --git a/deploy/helm/fuzefront/templates/email-service.yaml b/deploy/helm/fuzefront/templates/email-service.yaml index 6d58e5ac..a823a046 100644 --- a/deploy/helm/fuzefront/templates/email-service.yaml +++ b/deploy/helm/fuzefront/templates/email-service.yaml @@ -43,6 +43,48 @@ spec: value: {{ .Values.emailService.email.provider | quote }} - name: EMAIL_FROM value: {{ .Values.emailService.email.from | quote }} + {{- if eq .Values.emailService.email.provider "smtp" }} + {{- $smtp := .Values.emailService.email.smtp | default dict }} + {{- if $smtp.host }} + # SMTP transport. Wired ONLY when a host is configured, because the + # service's in-code default is localhost:1025 (a dev mailhog) and + # nodemailer will happily "succeed" against nothing: the pod stays + # healthy, /health stays green, and every message is silently dropped. + # That is precisely how signup email-verification and password-reset + # ran live-dead in prod — enabled, healthy, delivering nothing. + - name: SMTP_HOST + value: {{ $smtp.host | quote }} + - name: SMTP_PORT + value: {{ $smtp.port | default 587 | quote }} + - name: SMTP_SECURE + value: {{ $smtp.secure | default false | quote }} + # Credentials come from the SealedSecret and are NEVER in values. + # Hard refs (no `optional`) — an authenticated relay like Zoho rejects + # anonymous mail, so a missing credential must CrashLoop the pod at + # start, loudly, rather than degrade into dropping mail at runtime. + - name: SMTP_USER + valueFrom: + secretKeyRef: + name: {{ include "fuzefront.secretName" . }} + key: SMTP_USER + - name: SMTP_PASS + valueFrom: + secretKeyRef: + name: {{ include "fuzefront.secretName" . }} + key: SMTP_PASS + {{- else if .Values.securityService.requireEmailVerification }} + {{- /* + Derive the requirement from the SOURCE OF TRUTH, never restate it. + securityService.requireEmailVerification: true means every new signup + is blocked until it receives a verification mail. With no SMTP host, + that mail goes to the localhost:1025 fallback and vanishes — so the + flag would lock out EVERY new user while the chart renders happily. + values-prod.yaml documents this ordering in prose ("prove the sender → + then flip this to true"); prose is not a guard. This is. + */}} + {{- fail "securityService.requireEmailVerification is true but emailService.email.smtp.host is empty — verification mail would go to the localhost:1025 dev fallback and be silently dropped, locking out every new signup. Configure a real SMTP host (and seal SMTP_USER/SMTP_PASS) before enabling verification." }} + {{- end }} + {{- end }} # Gate on existingSecret TOO (same reason as sms-service): in prod the # inline value is empty and the real key lives in the SealedSecret. # `optional: true` so the pod still starts on the SMTP provider path, diff --git a/deploy/helm/fuzefront/values-prod.yaml b/deploy/helm/fuzefront/values-prod.yaml index 59582abf..8d079b12 100644 --- a/deploy/helm/fuzefront/values-prod.yaml +++ b/deploy/helm/fuzefront/values-prod.yaml @@ -49,14 +49,20 @@ securityService: # Fresh address (absent in prod) so the idempotent seed-admin hook does a clean # insert with the sealed FUZEFRONT_ADMIN_PASSWORD. See PR #213 (password reseal). adminEmail: admin@fuzefront.com - # Signup email verification (PR #275) — DELIBERATELY OFF. - # While false the code auto-verifies every new account, i.e. verification is - # effectively not enforced. That is the SAFE state right now: email-service is - # enabled but its SMTP sender is NOT yet proven end-to-end (no SMTP_HOST is - # wired; it defaults to a mailhog-style localhost:1025 and silently drops mail). - # Flipping this to true before a real sender is proven would bounce EVERY new - # signup — they would be created unverified and never receive the mail needed to - # verify. Sequence: prove the sender → then flip this to true via GitOps. + # Signup email verification (PR #275) — OFF until the sender is proven. + # While false the code auto-verifies every new account, i.e. verification is not + # enforced. That remains the SAFE state until a real SMTP sender is proven + # end-to-end: email-service is enabled and healthy, but with no SMTP host it + # falls back in-code to localhost:1025 and silently drops every message. + # + # This is no longer guarded by prose alone: the chart FAILS TO RENDER if this is + # true while emailService.email.smtp.host is empty, because flipping it without a + # deliverable sender locks out EVERY new signup (created unverified, never + # mailed). Go-live sequence — deploy/contabo/SEAL_PROD_SECRETS.md: + # 1. owner seals SMTP_USER + SMTP_PASS + # 2. set emailService.email.smtp.host below (GitOps) + # 3. PROVE a real message reaches a real inbox ← password reset revives here + # 4. only then flip this to true, in a deploy window requireEmailVerification: false image: repository: ghcr.io/izzywdev/fuzefront-security-service @@ -102,9 +108,23 @@ emailService: image: repository: ghcr.io/izzywdev/fuzefront-email-service tag: 1fbd206c5b2a - # Provider stays "smtp" (chart default) until a sender is proven end-to-end. - # NOTE: securityService.requireEmailVerification MUST stay false until then — - # see the security block below. + email: + provider: "smtp" + from: "noreply@fuzefront.com" + smtp: + # EMPTY = the service falls back in-code to localhost:1025 and SILENTLY + # DROPS every message. That fallback is why signup email-verification and + # password reset have both been live-dead in prod: pod healthy, /health + # green, zero delivery. + # + # Set this to the real relay (Zoho: smtp.zoho.com) ONLY once the owner has + # sealed SMTP_USER + SMTP_PASS — the chart mounts them as HARD secretKeyRefs + # on this path, so setting a host without sealing them CrashLoops the pod at + # start (loud) instead of dropping mail at runtime (silent). That trade is + # deliberate. See deploy/contabo/SEAL_PROD_SECRETS.md. + host: "" + port: 587 + secure: false # STARTTLS on 587; true for implicit TLS on 465 # sms-service — Twilio Verify dispatcher behind phone 2FA (SMS/voice, PR #274). # Its image is built by release.yml, but its repository was likewise absent from diff --git a/deploy/helm/fuzefront/values.yaml b/deploy/helm/fuzefront/values.yaml index f7ef8300..2144d000 100644 --- a/deploy/helm/fuzefront/values.yaml +++ b/deploy/helm/fuzefront/values.yaml @@ -291,6 +291,20 @@ emailService: email: provider: "smtp" # "sendgrid" or "smtp" from: "noreply@fuzefront.com" + # SMTP transport config (provider: "smtp"). NON-secret only — the credentials + # (SMTP_USER / SMTP_PASS) come from the SealedSecret, never from values. + # + # Leave `host` EMPTY for local/dev: the service then falls back to its in-code + # localhost:1025 (mailhog) default, which is correct for a dev mail catcher. + # In any environment that must deliver real mail, host MUST be set — see the + # smtp.hostRequired guard in templates/email-service.yaml. That in-code + # fallback is silent: with no host the pod is healthy, /health is green, and + # every message is dropped. That is exactly how signup email-verification and + # password-reset sat live-dead in prod. + smtp: + host: "" # e.g. smtp.zoho.com — empty = dev mailhog fallback + port: 587 + secure: false # true for implicit TLS (465); false for STARTTLS (587) resources: requests: { cpu: 50m, memory: 128Mi } limits: { cpu: 500m, memory: 256Mi } diff --git a/docs/planning/design-first-ui-pipeline.md b/docs/planning/design-first-ui-pipeline.md new file mode 100644 index 00000000..a97fb6db --- /dev/null +++ b/docs/planning/design-first-ui-pipeline.md @@ -0,0 +1,150 @@ +# Design-first UI pipeline (product-designer → frames PR → RED QA → components → flows) + +> **Status: approved, in progress.** Authored 2026-07-17. This is the working plan +> of record for the UX/UI design pipeline; it supersedes the PenPot design-review +> gate described in `CLAUDE.md` (HTML frames are authoritative — see *Corrections* +> §7 below). +> +> **Resolved since drafting** (the plan's open questions, now settled with evidence): +> - **Repo visibility / Pages** — the repo is **public**, so Pages needs no +> Enterprise Cloud. Pages is **enabled** with `build_type=workflow` +> (`https://izzywdev.github.io/FuzeFront/`). The Actions source was chosen over a +> branch source because a branch source serves only `/` or `/docs`, and frames +> live at `design/frames/**`. +> - **"Actions cannot create PRs here"** — confirmed, and it is a deliberate +> security choice, not a gap: `can_approve_pull_request_reviews: false`, and +> GitHub bundles create-PR with approve-PR, which on a deploy-on-push `master` +> with required reviews would be a self-approval path to prod. So the plan's +> assumption holds — **the initiator agent opens the frames PR**, CI never does. +> - **`product-designer` exists** — landed with the `contract-designer` / +> `frontend-engineer` corrections (PR #300). +> +> **Not yet built** (sequencing step 1 remainder): `design/frames/_template/`, +> `scripts/stamp-frames.mjs`, `gate-frames-first`, `gate-frames-stamped`, the +> design-approval issue form + workflow, and Pages publishing for +> `design/frames/**`. + +## Context + +**The problem.** Seven backend PRs merged today (#285, #288–#293). Every capability is reachable **only by curl** — password reset, MFA/2FA management, API tokens, devices/sessions, social link/unlink, AuthZ roles. No UI, no e2e (#271 shipped `test.fixme` scaffolds, not tests). Backend real, product absent. + +**Why it happened — and the fix you named.** My earlier UI attempt produced zero tool calls and I moved on. But "the agent failed" is the shallow cause. **The real cause: nothing made governance mechanical.** Pushing feature-UI with no approved frames was *possible*. That's the session's recurring pattern — a rule nobody can skip beats a step someone is supposed to remember. + +**Outcome intended.** A design-first pipeline where the process is enforced by CI, not by anyone's diligence, and which generalises into the UX/UI design/QA platform. + +## Corrections to my first draft (all yours, all right) + +1. **The frames are NOT the frontend-engineer's job.** They belong to a new **`product-designer`** agent — a UX/UI expert operating on **product requirements and user stories**. A frontend-engineer authoring the design is exactly the bias problem `contract-designer` exists to prevent on the backend: the implementer must not also write the spec they're measured against. **This mirrors contract-first, one-for-one.** +2. **Frames are always their own PR, and its only content.** Nothing else rides along. +3. **Merging frames triggers RED QA (TDD)** — UX QA agents write failing tests for the flows *before* implementation exists. Then frontend-engineers implement components → aggregate into flows → ship as packages. +4. **The frames declare the build inventory** — the flows, React components, and npm packages to be created are *rendered in the frames and approved with them*. The frames are the architectural contract, not just pictures. +5. **Reject ≠ close.** Reject **re-dispatches `product-designer`** for an improving iteration. Closing throws the thread away. +6. **Per-flow approval.** Approving one flow injects its `approved: true` and unblocks *that* flow's implementation — no waiting for the whole set. +7. **PenPot is parked** — you're backing HTML-frames-as-authoritative; CLAUDE.md's PenPot design-review gate gets updated (it currently contradicts `ui-frame-contract`). + +## The pipeline + +``` +product requirement / user story + │ + ▼ +product-designer ──► frames PR (frames ONLY, own branch) + │ CI: gate-ds-conformance · gate-frames-schema · gate-ux-policy + ▼ + approval (in-frame link, per flow) ──reject──► product-designer iterates + │ approve + ▼ + merge ──► triggers UX QA agents ──► Playwright specs for each flow: ALL RED (TDD) + │ + ▼ +frontend-engineers ──► components (DS-first) ──► aggregated into flow orchestrators + ──► shipped as npm packages ──► merged ──► deployed ──► specs go GREEN +``` + +**`gate-frames-first` (the CI rule that makes this real):** a PR touching feature UI (`frontend/src/**`, `packages/*-ui/**`) **fails** unless an approved `design/frames//manifest.json` exists covering it. This is what "pushing the code should have triggered the process" becomes — enforcement, not etiquette. It is also the rule that would have caught *me*. + +## New agent: `product-designer` + +- **Owns:** user story → flows, screens, states, and the **build inventory** (flows / React components / npm packages) → navigable HTML frames + manifest. Sole author of `design/frames/**`. +- **Does NOT:** write feature code, tests, or deploy wiring. Sequential gate, like `contract-designer`. +- **Consults:** `fuzefront-expert` for repo reality (this is where "you're planning SMS? we already have a Kafka topic" gets caught — the miss that started this thread). +- **Skills:** `ui-frame-contract`, `frontend-design`, `design-system-conformance`. +- Lives in `.claude/agents/product-designer.md` (tracked in git, propagates to remote runs) → **promote to FuzeSDLC** so every repo gets it. + +## Frames: what's in them + +Established precedent to copy: `design/frames/billing-invoices/` (`index.html` navigable entry · `01-*.html` ordered sequence · `tokens.css` · `manifest.json` with `approved/approvedBy/approvedAt` + `contract{openapi,client,endpoints,component,featureFlag}` + `data-*` hooks Playwright drives). + +**Additions:** +- **Build inventory rendered in `index.html`** and mirrored in the manifest: + ```json + "build": { + "flows": [{ "id": "reset", "orchestrator": "PasswordResetFlow", "route": "/reset" }], + "components": ["OtpInput", "PasswordStrengthMeter", "ResetRequestForm"], + "packages": ["@fuzefront/account-security-ui"] + } + ``` + Approving the frames approves the component/package plan — so implementation can't quietly invent a different architecture. +- **Per-flow approval** — `approved` moves onto each `flows[]` entry, so one flow can unblock while another iterates. +- **States are contract, not decoration** — loading/empty/error and the real fail-closed cases (unlink-last-method → 409; `hasPassword: null` → "set a password first"). Frames showing only the happy path produce UI that only handles the happy path. + +## Approval mechanism + +**Enable GitHub Pages** (you asked; I have `gh`) publishing `design/frames/**` → frames become a real clickable URL (yes — that shared-belongings link was Pages). Note: Pages on a **private** repo needs Enterprise Cloud; if this repo is private, fallback is opening `index.html` locally — the approval links are absolute GitHub URLs, so they work from `file://` with zero infra. + +In-frame control → prefilled **GitHub Issue Form** (`feature`/`flow`/`frames_hash`/`decision` prefilled; a **Notes textarea** is your prompt). Automating the approval decision itself: **deferred, per your call.** + +**`design-approval.yml`** — `on: issues: [opened]` only (edits can't re-fire), `concurrency: design-approval-`, then four gates: +1. **Authorization** — actor must be owner/CODEOWNER. *Anyone can open an issue*; without this, anyone could approve a design. +2. **Staleness** — recompute the content hash of `design/frames//**`; mismatch → "frames changed since you viewed them" + re-dispatch. **Load-bearing:** an approval provably binds to the exact frames you saw, so a stale bookmarked link can never approve frames you never looked at. +3. **Idempotency** — read the manifest first; already approved at this hash → no-op + comment. **A double-click is not a second approval.** (Your double-trigger concern.) +4. **Write** — flip the flow's `approved` via the **Contents API** (server-side commits are signed → satisfies `required_signatures`) onto the **frames PR head branch, never master** (master is deploy-on-push + review-required). + +**Reject** → notes onto the PR + **re-dispatch `product-designer`**; issue stays open until resolved. + +Actions **cannot create PRs** here (verified). Fine: the **initiator agent that ordered the design service opens the frames PR**; the workflow only pushes to that existing branch and comments. + +**Anti-drift:** `scripts/stamp-frames.mjs` derives the hash; **`gate-frames-stamped`** recomputes it in CI. Otherwise the stamp is another hand-maintained mirror — the exact pattern behind four of today's defects (`release.yml` 7-vs-5 anchors, kafka-topics vs TOPICS, openapi vs `authz.ts`, `core/dist` vs `src`). + +## Feature flags + the visibility gap you caught + +Flags default OFF ⇒ **prod-smoke and Playwright would never see the feature, so its e2e could never go green.** Policy update: define a **builders audience** — you + the QA agents' synthetic accounts — that flag targeting includes. Prod smoke authenticates as a builder, sees the flagged feature, and can prove it before general rollout. Without this, "flag it OFF then verify in prod" is self-defeating. + +## Template → FuzeSDLC (the part that outlives this feature) + +``` +design/frames/_template/{index.html,frame.html,approval-bar.html,manifest.schema.json,README.md} +.github/ISSUE_TEMPLATE/design-approval.yml +.github/workflows/design-approval.yml # workflow_call — lives in FuzeSDLC +.github/workflows/gate-frames-first.yml # UI code requires approved frames +scripts/stamp-frames.mjs +.claude/agents/product-designer.md +``` +One implementation in **FuzeSDLC**, called by every repo, distributed by the bootstrap. Repos keep only `design/frames/**`. + +**Why this is already the platform:** every decision lands as structured, CI-validated data (`manifest.schema.json` + the issue thread as audit trail). A future UX/UI app reads the manifests; nothing needs re-authoring. + +## The six features +`account-security` · `password-reset` · `mfa-management` · `devices-sessions` · `api-tokens` · `authz-admin` — each a frames PR authored by `product-designer`, each declaring its flows/components/packages. + +## Critical files +- `.claude/skills/ui-frame-contract/SKILL.md` — authoritative procedure +- `design/frames/billing-invoices/{index.html,manifest.json,tokens.css}` — **reference to copy** +- `design-system/styles.css`, `design-system/tokens/*.css` +- `packages/security/openapi.yaml` — contract the frames bind to +- `CLAUDE.md` — remove the PenPot/HTML contradiction + +## Verification +- **Frames PR**: `index.html` walks the flow; DS-conformant (no raw values); manifest schema-valid; inventory rendered; `gate-frames-stamped` green. +- **Approval**: double-click = no-op; stale link = rejected; non-owner = refused. +- **Post-merge**: QA specs exist and are **RED** before implementation (proves TDD, not retrofitted tests). +- **Post-implementation**: specs green vs frames → built app → live app (as a builder-audience user). +- **`gate-frames-first`**: a UI PR without approved frames fails. Test it deliberately. + +## Sequencing +1. Enable Pages · scaffold `_template` + `product-designer` + the two gates + issue form (this is the platform). +2. `product-designer` authors all six frames PRs in parallel. +3. You approve per flow. +4. Each approved flow: RED QA → components → orchestrator → package → green. +5. Promote the template + agent to FuzeSDLC; re-bootstrap. +