From 6c8e811a65dc13598763cc208f8f593ccc466531 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 18 Aug 2026 13:30:43 +0000
Subject: [PATCH 1/4] chore: bootstrap FuzeSDLC capability installer (wave 6)
Adopt the FuzeSDLC canonical capability set: schema-validate, core-agents,
workflows, community, roles, npm-registry, node-toolchain (engines/.nvmrc
only), entity-identity scaffolding, design-system conformance gate, skills,
portal-registration, and hardening pins across .github/workflows.
mf-remote is explicitly skipped: FuzeAgent shares React via a bare array
(no singleton semantics) and currently bundles its own React copy. Converting
that to a real MF singleton is exactly the change that produces a green build
with a runtime-only "Invalid hook call", so it is deferred to its own PR
with a real browser check. React stays at ^18 in services/ui-react and
services/ui; only engines.node/npm and .nvmrc were raised.
The frozen agent-templates/contracts/a2a/v1/ A2A contract is untouched, and
auth (services/orchestrator/auth.py, agent-templates/a2a/authz.py) was not
touched, per the tracked hand-rolled-HS256 finding.
---
.claude/agents/agile-manager.md | 2 +-
.claude/agents/backend-engineer.md | 4 +-
.claude/agents/contract-designer.md | 2 +-
.claude/agents/devops-engineer.md | 4 +-
.claude/agents/security.md | 1 +
.claude/agents/test-engineer.md | 4 +-
.claude/skills/api-contract-first/SKILL.md | 35 +
.../skills/design-system-conformance/SKILL.md | 51 +
.../skills/design-system-inheritance/SKILL.md | 17 +
.claude/skills/doc-validity/SKILL.md | 10 +
.claude/skills/feature-flags/SKILL.md | 95 ++
.claude/skills/feature-tech-planning/SKILL.md | 65 ++
.claude/skills/fuzefront-ui-package/SKILL.md | 34 +
.claude/skills/logging/SKILL.md | 168 ++++
.claude/skills/managed-agents-roles/SKILL.md | 92 ++
.claude/skills/mobile-conformance/SKILL.md | 10 +
.claude/skills/model-cascade/SKILL.md | 106 ++
.claude/skills/repo-hardening/SKILL.md | 18 +
.claude/skills/service-cli/SKILL.md | 10 +
.claude/skills/ticket-creator/SKILL.md | 41 +
.../skills/ticket-creator/backend/SKILL.md | 49 +
.claude/skills/ticket-creator/bug/SKILL.md | 51 +
.claude/skills/ticket-creator/devops/SKILL.md | 35 +
.claude/skills/ticket-creator/docs/SKILL.md | 29 +
.claude/skills/ticket-creator/epic/SKILL.md | 57 ++
.../skills/ticket-creator/frontend/SKILL.md | 47 +
.claude/skills/ticket-creator/qa/SKILL.md | 26 +
.../ticket-creator/qa/functional/SKILL.md | 25 +
.../ticket-creator/qa/integration/SKILL.md | 25 +
.../ticket-creator/qa/load-stress/SKILL.md | 25 +
.../ticket-creator/qa/security/SKILL.md | 25 +
.../skills/ticket-creator/qa/unit/SKILL.md | 25 +
.claude/skills/ticket-creator/story/SKILL.md | 56 ++
.claude/skills/ticket-creator/ux/SKILL.md | 38 +
.claude/skills/ticket-enforcer/SKILL.md | 187 ++++
.claude/skills/ticket-reviewer/SKILL.md | 61 ++
.claude/skills/ticket-reviewer/bug/SKILL.md | 31 +
.../skills/ticket-reviewer/dev-task/SKILL.md | 51 +
.../skills/ticket-reviewer/devops/SKILL.md | 24 +
.claude/skills/ticket-reviewer/docs/SKILL.md | 22 +
.claude/skills/ticket-reviewer/epic/SKILL.md | 27 +
.claude/skills/ticket-reviewer/qa/SKILL.md | 40 +
.claude/skills/ticket-reviewer/story/SKILL.md | 28 +
.claude/skills/ui-frame-contract/SKILL.md | 48 +
.claude/skills/ui-runtime-validation/SKILL.md | 109 +++
.claude/skills/verification-protocol/SKILL.md | 26 +
.fuze/identifier-allowlist.txt | 25 +
.fuze/installed.json | 924 ++++++++++++++++++
.fuze/repo-manifest.schema.json | 301 ++++++
.github/labels.json | 13 +
.github/workflows/a2a-maintain.yml | 2 +-
.github/workflows/auto-merge.yml | 9 +-
.github/workflows/claude-auto-pr.yml | 2 +-
.github/workflows/claude-ci-autofix.yml | 2 +-
.github/workflows/claude.yml | 33 +-
.github/workflows/design-approval.yml | 298 ++++++
.github/workflows/design-review-notify.yml | 33 +
.github/workflows/gate-frames-stamped.yml | 36 +
.github/workflows/gate-line-endings.yml | 2 +-
.github/workflows/governance-nightly.yml | 21 +-
.github/workflows/governance-sync.yml | 6 +-
.github/workflows/harden-gate.yml | 291 +++++-
.github/workflows/helm-validate.yml | 80 +-
.github/workflows/label-sync.yml | 71 ++
.github/workflows/mcp-maintain.yml | 7 +-
.github/workflows/nightly-integration.yml | 6 +-
.github/workflows/pages-frames.yml | 57 ++
.github/workflows/publish-expert.yml | 67 +-
.npmrc | 35 +
.nvmrc | 1 +
agent-templates/sync/role_loader.py | 20 +-
containers/templates/dev-typescript/.npmrc | 35 +
.../templates/dev-typescript/package.json | 12 +-
e2e/.npmrc | 35 +
e2e/package.json | 6 +-
scripts/__tests__/test_gate_identifier.py | 208 ++++
scripts/build-frames-site.mjs | 333 +++++++
scripts/check-registration.mjs | 213 ++++
scripts/gate_ds_conformance.py | 399 ++++++++
scripts/gate_identifier.py | 783 +++++++++++++++
scripts/gate_manifest.py | 188 ++++
scripts/gate_pagination.py | 261 +++++
scripts/stamp-frames.mjs | 208 ++++
scripts/sync-chart-files.sh | 77 +-
services/ui-react/.npmrc | 35 +
services/ui-react/package.json | 4 +
services/ui/.npmrc | 35 +
services/ui/package.json | 6 +-
88 files changed, 7009 insertions(+), 107 deletions(-)
create mode 100644 .claude/skills/api-contract-first/SKILL.md
create mode 100644 .claude/skills/design-system-conformance/SKILL.md
create mode 100644 .claude/skills/design-system-inheritance/SKILL.md
create mode 100644 .claude/skills/doc-validity/SKILL.md
create mode 100644 .claude/skills/feature-flags/SKILL.md
create mode 100644 .claude/skills/feature-tech-planning/SKILL.md
create mode 100644 .claude/skills/fuzefront-ui-package/SKILL.md
create mode 100644 .claude/skills/logging/SKILL.md
create mode 100644 .claude/skills/managed-agents-roles/SKILL.md
create mode 100644 .claude/skills/mobile-conformance/SKILL.md
create mode 100644 .claude/skills/model-cascade/SKILL.md
create mode 100644 .claude/skills/repo-hardening/SKILL.md
create mode 100644 .claude/skills/service-cli/SKILL.md
create mode 100644 .claude/skills/ticket-creator/SKILL.md
create mode 100644 .claude/skills/ticket-creator/backend/SKILL.md
create mode 100644 .claude/skills/ticket-creator/bug/SKILL.md
create mode 100644 .claude/skills/ticket-creator/devops/SKILL.md
create mode 100644 .claude/skills/ticket-creator/docs/SKILL.md
create mode 100644 .claude/skills/ticket-creator/epic/SKILL.md
create mode 100644 .claude/skills/ticket-creator/frontend/SKILL.md
create mode 100644 .claude/skills/ticket-creator/qa/SKILL.md
create mode 100644 .claude/skills/ticket-creator/qa/functional/SKILL.md
create mode 100644 .claude/skills/ticket-creator/qa/integration/SKILL.md
create mode 100644 .claude/skills/ticket-creator/qa/load-stress/SKILL.md
create mode 100644 .claude/skills/ticket-creator/qa/security/SKILL.md
create mode 100644 .claude/skills/ticket-creator/qa/unit/SKILL.md
create mode 100644 .claude/skills/ticket-creator/story/SKILL.md
create mode 100644 .claude/skills/ticket-creator/ux/SKILL.md
create mode 100644 .claude/skills/ticket-enforcer/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/bug/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/dev-task/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/devops/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/docs/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/epic/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/qa/SKILL.md
create mode 100644 .claude/skills/ticket-reviewer/story/SKILL.md
create mode 100644 .claude/skills/ui-frame-contract/SKILL.md
create mode 100644 .claude/skills/ui-runtime-validation/SKILL.md
create mode 100644 .claude/skills/verification-protocol/SKILL.md
create mode 100644 .fuze/identifier-allowlist.txt
create mode 100644 .fuze/installed.json
create mode 100644 .fuze/repo-manifest.schema.json
create mode 100644 .github/labels.json
create mode 100644 .github/workflows/design-approval.yml
create mode 100644 .github/workflows/design-review-notify.yml
create mode 100644 .github/workflows/gate-frames-stamped.yml
create mode 100644 .github/workflows/label-sync.yml
create mode 100644 .github/workflows/pages-frames.yml
create mode 100644 .npmrc
create mode 100644 .nvmrc
create mode 100644 containers/templates/dev-typescript/.npmrc
create mode 100644 e2e/.npmrc
create mode 100644 scripts/__tests__/test_gate_identifier.py
create mode 100644 scripts/build-frames-site.mjs
create mode 100644 scripts/check-registration.mjs
create mode 100644 scripts/gate_ds_conformance.py
create mode 100644 scripts/gate_identifier.py
create mode 100644 scripts/gate_manifest.py
create mode 100644 scripts/gate_pagination.py
create mode 100644 scripts/stamp-frames.mjs
create mode 100644 services/ui-react/.npmrc
create mode 100644 services/ui/.npmrc
diff --git a/.claude/agents/agile-manager.md b/.claude/agents/agile-manager.md
index 53f62e6..1ee88be 100644
--- a/.claude/agents/agile-manager.md
+++ b/.claude/agents/agile-manager.md
@@ -4,7 +4,7 @@ model: sonnet
description: Owns ONLY project/delivery coordination — Jira/Confluence issue & sprint management, ticket lifecycle, status reporting, and team communication (Atlassian + Slack). Turns work into well-formed tickets, tracks them, and reports status. Does NOT write product code, UI, tests, or deploy wiring. Use for backlog grooming, sprint planning, triage, status reports, and cross-repo @claude delegation coordination.
# Owns the Atlassian + Slack MCP servers (project tracking + team comms). It is the
# ONLY agent granted these — coordination is reserved here, away from the code agents.
-tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, mcp__plugin_atlassian_atlassian, mcp__plugin_slack_slack
+tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, mcp__plugin_atlassian_atlassian, mcp__plugin_slack_slack, mcp__github__list_issues, mcp__github__issue_read, mcp__github__issue_write, mcp__github__add_issue_comment, mcp__github__list_pull_requests, mcp__github__pull_request_read, mcp__github__search_issues
skills: [ticket-creator, ticket-reviewer, ticket-enforcer, model-cascade]
---
diff --git a/.claude/agents/backend-engineer.md b/.claude/agents/backend-engineer.md
index a4bcb03..b260f03 100644
--- a/.claude/agents/backend-engineer.md
+++ b/.claude/agents/backend-engineer.md
@@ -3,7 +3,7 @@ name: backend-engineer
model: sonnet
description: Implements ONLY the backend slice of a feature — HTTP API/services, business logic, DB schema/migrations, events, and the backend's own unit tests — against a frozen API contract. Does NOT build UI, the independent test suite, deploy wiring, or docs. Use for backend implementation in a contract-first fan-out.
# Figma is reserved for frontend-engineer; pure-code agent gets core tools only (no MCP).
-tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite
+tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, mcp__github__list_issues, mcp__github__issue_read, mcp__github__pull_request_read, mcp__github__list_pull_requests
skills: [api-contract-first, feature-flags, logging, verification-protocol, model-cascade]
---
@@ -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 e6c32af..ccae156 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/devops-engineer.md b/.claude/agents/devops-engineer.md
index b7419dd..8a8d257 100644
--- a/.claude/agents/devops-engineer.md
+++ b/.claude/agents/devops-engineer.md
@@ -4,7 +4,7 @@ model: sonnet
description: Implements ONLY the deploy/CI slice — Helm chart + values, Argo Application wiring, the release/CI image matrix + tag-bump, infra-request manifests, and SealedSecrets scaffolding. Does NOT write app code, UI, or the test suite. Use for the devops stream in a contract-first fan-out.
# Owns the Cloudflare MCP servers (edge/DNS/Workers/observability) + the AWS plugin skills.
# Figma is reserved for frontend-engineer. Cloud/edge tooling is reserved here.
-tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, mcp__plugin_cloudflare_cloudflare-api, mcp__plugin_cloudflare_cloudflare-bindings, mcp__plugin_cloudflare_cloudflare-builds, mcp__plugin_cloudflare_cloudflare-docs, mcp__plugin_cloudflare_cloudflare-observability
+tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, mcp__plugin_cloudflare_cloudflare-api, mcp__plugin_cloudflare_cloudflare-bindings, mcp__plugin_cloudflare_cloudflare-builds, mcp__plugin_cloudflare_cloudflare-docs, mcp__plugin_cloudflare_cloudflare-observability, mcp__github__list_pull_requests, mcp__github__pull_request_read, mcp__github__get_check_run, mcp__github__actions_list, mcp__github__actions_get, mcp__github__list_issues, mcp__github__issue_read, mcp__github__add_issue_comment, mcp__github__list_commits, mcp__github__get_commit, mcp__github__list_releases, mcp__github__get_latest_release
skills: [repo-hardening, verification-protocol, model-cascade]
---
@@ -26,7 +26,7 @@ Run these through the `repo-hardening` skill so every repo lands in parity with
- **Never hand-deploy to prod** and **never edit the shared-infra (FuzeInfra) repo** — prod is GitOps (Argo syncs from git); cluster/node changes are *declared* (deploy/terraform + deploy/argocd) and reconciled by the infra platform. Local only = Helm/Skaffold on kind.
## How
-**Skills (load these):** `repo-hardening` (branch-protection ruleset, signing-safe bot pushes, release/tag-bump), `observability` (metrics/dashboards/alerts are part of your slice), `well-architected` (reliability/cost/ops trade-offs), `verification-before-completion` (render + validate before reporting) + repo context from the repo's expert agent (+ `fuzeinfra-expert` for cluster-contract questions). For **edge/DNS/CDN/Workers** work use the Cloudflare MCP + `cloudflare`, `wrangler`, `workers-best-practices`, `cloudflare-one` skills; for **cloud** work use the AWS plugin skills (`aws-iam`, `aws-cloudformation`/`aws-cdk`, `aws-serverless`, `aws-containers`, `aws-secrets-manager`, `aws-observability`, `aws-billing-and-cost-management`). The apex stays on its CDN of record — Cloudflare/edge tooling is for app-host DNS/TLS/CDN and edge functions, never a hand-deploy to the GitOps prod cluster. Follow the platform rules: GitOps-only, no kubeconfig, secrets sealed/ref'd never inline, per-service `enabled` gate + resource limits + node affinity. Validate with `helm lint` + `kubeconform` + `actionlint`. Never enter plan mode/brainstorming; push continuously; if blocked, push + RETURN `BLOCKED: `.
+**Skills (load these):** `repo-hardening` (branch-protection ruleset, signing-safe bot pushes, release/tag-bump), `observability` (metrics/dashboards/alerts are part of your slice), `well-architected` (reliability/cost/ops trade-offs), `verification-before-completion` (render + validate before reporting) + repo context from the repo's expert agent (+ `fuzeinfra-expert` for cluster-contract questions). For **prod cluster reads** (deploy verification, pod status, logs): invoke the FuzeInfra `cluster-query` workflow — read-only `kubectl` against prod, self-service from any repo: `gh workflow run cluster-query.yml --repo izzywdev/FuzeInfra -f kubectl_args='-n get pods -o wide'`. Permitted verbs: `get`, `describe`, `logs`, `top`, `events`, `version`. No mutating verbs; `--raw` is blocked. Never relay cluster state through a human — this is the designated self-service path. For **edge/DNS/CDN/Workers** work use the Cloudflare MCP + `cloudflare`, `wrangler`, `workers-best-practices`, `cloudflare-one` skills; for **cloud** work use the AWS plugin skills (`aws-iam`, `aws-cloudformation`/`aws-cdk`, `aws-serverless`, `aws-containers`, `aws-secrets-manager`, `aws-observability`, `aws-billing-and-cost-management`). The apex stays on its CDN of record — Cloudflare/edge tooling is for app-host DNS/TLS/CDN and edge functions, never a hand-deploy to the GitOps prod cluster. Follow the platform rules: GitOps-only, no kubeconfig, secrets sealed/ref'd never inline, per-service `enabled` gate + resource limits + node affinity. Validate with `helm lint` + `kubeconform` + `actionlint`. Never enter plan mode/brainstorming; push continuously; if blocked, push + RETURN `BLOCKED: `.
## Provisioning a node via deploy/terraform (the infra-request contract — get it exact)
A consumer repo NEVER provisions or holds cloud creds; it DECLARES a node and the infra platform's
diff --git a/.claude/agents/security.md b/.claude/agents/security.md
index 46b9cba..3717edb 100644
--- a/.claude/agents/security.md
+++ b/.claude/agents/security.md
@@ -2,6 +2,7 @@
name: security
model: opus
description: Owns the security posture across the Fuze family — CVE/dependency response (triage the SARIF from gate-sast/gate-dependency-scan/gate-secret-scan), secret hygiene and rotation, threat modeling, supply-chain/SBOM, and incident coordination. Configures and coordinates; does NOT write feature code, UI, or deploy charts (it hands findings to the owning implementer/devops). Use for vulnerability triage, secret leaks, security review of a design, or incident response.
+tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, mcp__github__run_secret_scanning, mcp__github__list_issues, mcp__github__issue_read, mcp__github__issue_write, mcp__github__add_issue_comment, mcp__github__pull_request_read, mcp__github__get_check_run
skills: [verification-protocol, repo-hardening, model-cascade]
---
diff --git a/.claude/agents/test-engineer.md b/.claude/agents/test-engineer.md
index 53212bb..a3949f7 100644
--- a/.claude/agents/test-engineer.md
+++ b/.claude/agents/test-engineer.md
@@ -3,7 +3,7 @@ name: test-engineer
model: sonnet
description: Writes the INDEPENDENT acceptance/contract/integration test suite against the frozen spec — the objective verification that an implementation actually works. Does NOT implement the feature. Use as the verification stream in a contract-first fan-out, separate from the implementers.
# Figma is reserved for frontend-engineer; pure-code agent gets core tools only (no MCP).
-tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite
+tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite, mcp__github__list_issues, mcp__github__issue_read, mcp__github__pull_request_read, mcp__github__get_check_run
skills: [verification-protocol, ticket-creator, model-cascade]
---
@@ -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.
diff --git a/.claude/skills/api-contract-first/SKILL.md b/.claude/skills/api-contract-first/SKILL.md
new file mode 100644
index 0000000..8a17be4
--- /dev/null
+++ b/.claude/skills/api-contract-first/SKILL.md
@@ -0,0 +1,35 @@
+---
+name: api-contract-first
+description: Use BEFORE fanning out implementation on any feature/service with an HTTP API. Produces a frozen OpenAPI/Swagger contract (+ event schemas), generates the shared typed client, and sets up a mock server — so UI, backend, and tests can be built in parallel against one source of truth.
+---
+
+# API contract-first
+
+Freeze the contract before implementation fans out. The contract is the single synchronization point; everything else (UI, backend, tests, mock) derives from it, so contract drift becomes a compile error rather than an integration surprise.
+
+## When
+Any feature/service with a real HTTP API and >1 consumer (UI + backend, or multiple services). Pairs with the contract-first SDLC in CLAUDE.md and `feature-tech-planning`.
+
+## Procedure
+
+1. **Author the OpenAPI 3.1 spec.** Put it at `services//openapi.yaml` (or `contracts/.yaml`). Cover every route the service exposes: paths, methods, params, request/response schemas (reuse `components/schemas`), auth (the JWT bearer scheme), error shapes. If the service already exists, **derive the spec from the actual routes** and verify it matches by reading the route handlers — the spec must describe reality, not aspiration.
+2. **Add event schemas** for any async surface (FuzeFront: the Kafka Zod schemas in `shared/` — reference them; the contract isn't just HTTP).
+3. **Lint it**: `npx @stoplight/spectral-cli lint openapi.yaml` (add a sensible ruleset). Fix warnings.
+4. **Generate the shared typed client/types**: `npx openapi-typescript openapi.yaml -o packages/-client/src/schema.ts`, and expose typed request helpers. This IS the `@fuzefront/-client` npm package — private `publishConfig` (GitHub Packages, `@fuzefront`, `access: restricted`) + `repository` + wired into the release/publish pipeline. UI, backend, and tests all import these generated types.
+5. **Stand up a mock server from the contract** (Prism: `npx @stoplight/prism-cli mock openapi.yaml`, or MSW handlers generated from the schema) so the UI + test streams run before the backend exists.
+6. **PR the contract on its own branch, containing the contract and NOTHING ELSE, and freeze/merge it FIRST.**
+ The **only** permitted content is the **contract artifact set**: the spec (`openapi.yaml`/AsyncAPI) + event (Zod) schemas · the **generated** typed client (emitted by `openapi-typescript`, never hand-written behind it) · the approved UI frames (`design/frames//**`) + mock-server config · the contract's own version bump/changelog.
+
+ **Anything else makes it not a contract PR** — no route handlers, business logic, migrations, feature UI, behaviour tests, Helm/Argo/CI, or unrelated drive-bys. A contract PR is a **gate**; a gate that also carries implementation cannot be reviewed *as* a gate — the interface gets waved through while implementation rides along unexamined, and every downstream stream then builds on something nobody actually agreed to. If you are adding a non-contract file, it belongs in the implementation PR that comes *after* the freeze.
+
+ Only once it is **merged** do the implementer streams start — **all of them, gated only on the contract**: `backend-engineer`, `database-engineer`, `frontend-engineer` (against the mock), `test-engineer`, `frontend-test-engineer`, `devops-engineer`, **`mcp-engineer`, `cli-engineer`, `mobile-app-engineer`, `desktop-app-engineer`, `docs-maintainer`**. The MCP surface, CLI, mobile/desktop shells and consumer docs are projections of the same contract — building them from anything else is precisely how they drift from the API they claim to expose.
+7. **Changing the contract later** = amend the contract PR (deliberate ripple to all consumers), never diverge silently in an implementation.
+
+## Done checklist
+- [ ] `openapi.yaml` describes every real route (verified against handlers) + error shapes + auth
+- [ ] event schemas referenced
+- [ ] Spectral lint clean
+- [ ] `@fuzefront/-client` generated from the spec, private publishConfig + repository, in the publish pipeline
+- [ ] mock server command documented
+- [ ] contract is its own PR, **containing ONLY the contract artifact set** — `git diff --name-only origin/...HEAD` shows no implementation/infra file
+- [ ] frozen (merged) before ANY implementer stream starts — incl. mcp / cli / mobile / desktop / docs, not just backend+UI
diff --git a/.claude/skills/design-system-conformance/SKILL.md b/.claude/skills/design-system-conformance/SKILL.md
new file mode 100644
index 0000000..2b77624
--- /dev/null
+++ b/.claude/skills/design-system-conformance/SKILL.md
@@ -0,0 +1,51 @@
+---
+name: design-system-conformance
+description: Use when creating or reviewing UI / a design system. Enforces tokens-only (no raw hex/px/font), reuse-over-reinvent, and detecting code that should be EXTRACTED into a new DS primitive. The procedure behind gate-ds-conformance.
+---
+
+# design-system-conformance
+
+Use when building or reviewing **any** UI. Pairs with `design-system-inheritance`: inheritance says *extend the base, don't fork*; conformance says *use the tokens, don't reinvent, extract the duplicates*. This is the procedure behind the CI gate `gate-ds-conformance` (CLAUDE.baseline.md §6).
+
+## Tokens-only rule
+Feature code carries **no raw design values**. The gate hard-fails, in any UI file outside the DS package:
+- raw color literals: `#hex`, `rgb()/rgba()`, `hsl()/hsla()`
+- hard-coded spacing/sizing/type in `px` outside the token scale (`padding/margin/gap/width/height/font-size/line-height/border-radius: 12px`)
+- raw `font-family` strings
+
+Every color/spacing/type/radius comes from a **DS token** (`var(--ds-*)`, `tokens.*`, `theme.*`, `$token`, `@apply`). Two carve-outs the gate honors:
+- The **DS package itself is excluded** (`design-system/`, `packages/design-system/`, `tokens/`) — that is where token *definitions* legitimately live.
+- A rare, justified literal can be exempted with an inline escape comment containing `ds-conformance-disable` (also `-ignore`/`-allow`). Use sparingly; it is a documented exception, not an off-switch.
+
+## Reuse over reinvent
+Before writing a styled block, **search the DS for an existing primitive** and compose/extend it. Do not hand-roll a parallel button/card/field next to one the DS already ships. If what you need is missing, it belongs *in the DS* — see extraction below, not a one-off in feature code.
+
+## Detecting EXTRACTION candidates
+When the same ad-hoc styled block recurs across **>1 feature file** (≥ threshold, default 3), it should become a DS primitive. The gate fingerprints normalized styled blocks (whitespace/numbers stripped, so `p-4` and `p-6` collapse) and, on push to the default branch with `--emit-issues`, opens **one idempotent GitHub issue per candidate**:
+- label `ds-extraction`, stable marker `ds-fp:` (de-dupes across open+closed issues), mentions `@claude`
+- includes the recurring locations and a **proposed component spec**: props/variants/states/tokens + acceptance criteria.
+
+**Responding to such an issue:** `frontend-engineer` (the **sole** editor of `design-system/`) adds the primitive to the DS package, **tokens-only**, with a11y + RTL + a unit test, then refactors every listed call site to consume it. Extraction is design work — the gate only signals it.
+
+## CI gate
+`gate-ds-conformance` **hard-fails** raw values (exit 1); extraction issues are **advisory** (non-fatal — exit 0). First pass per repo is report-only (`|| true`); ratchet to enforcing once feature code is clean. See `verification-protocol` for proving the gate actually ran green.
+
+## Onboarding an existing repo into the Fuse design system (bidirectional)
+When bringing an **already-built** repo onto the family DS, the model is **bidirectional** — the repo-local DS *extends* the base (down) **and** worthy local primitives *graduate* to the base (up). Owned by the repo's `frontend-engineer` (sole DS owner). Baseline §6.2.
+
+1. **Build a repo-local DS if none exists.** Derive it from the repo's existing UI: harvest recurring colors/spacing/type into tokens and repeated blocks into components — run `gate_ds_conformance.py` (it surfaces both raw-value hotspots and the duplicated-block extraction candidates) to seed the inventory. This package becomes the repo's single styling source of truth.
+2. **Up-propagate (graduate to the base).** For each local primitive that should be a **global Fuze-family primitive**, open a promotion candidate using the **same `@claude` extraction-issue mechanism** (`ds-extraction` label + `ds-fp` fingerprint, idempotent) — one issue per candidate — routed to **FuzeFront's** frontend-engineer to land it in the base `@fuzefront/design-system` via PR. (Drive this manually or by pointing the gate's detector at the repo; the issue body's proposed-spec format is identical.)
+3. **Down-project (the base into the repo).** Make the repo-local DS **import and re-export / compose** the base tokens+primitives so the repo inherits the canonical look (unified Fuse experience) and keeps only its product-specific layer on top. Never copy or redefine a base primitive locally.
+4. **Graduation contract — what graduates vs stays local:**
+ - **Graduates** when generic/cross-product, free of product-specific business logic, and plausibly reused by ≥2 family repos (Button, Field, Modal, color/spacing/type tokens).
+ - **Stays local** when product-specific (domain widget, one-app layout, app-branded composition). When in doubt, keep it local until a second consumer appears, then graduate.
+ - `gate-ds-conformance` enforces **extends-not-forks**: shadowing/redefining a base primitive locally is a violation, not an extension — fix it upstream in the base or compose it, don't fork.
+
+Net steady state: **repo-local = base (inherited, down-projected) + thin product layer; worthy product primitives graduate up to the base.** Keep the inheritance layering explicit so a base upgrade flows through without a fork to reconcile.
+
+## Done checklist
+- [ ] No raw `#hex` / `rgb()` / `hsl()` / `px` spacing-size-type / `font-family` in feature code — all values are DS tokens.
+- [ ] Each escape (`ds-conformance-disable`) is a justified, documented exception — not a blanket bypass.
+- [ ] Reused existing DS primitives; no parallel hand-rolled component beside one the DS already has.
+- [ ] Any open `ds-extraction` issue triggered by this code is addressed (primitive added to DS + call sites refactored) or explicitly deferred.
+- [ ] `gate-ds-conformance` is green (no hard violations); confirmed via the actual run, not assumed.
diff --git a/.claude/skills/design-system-inheritance/SKILL.md b/.claude/skills/design-system-inheritance/SKILL.md
new file mode 100644
index 0000000..de7670a
--- /dev/null
+++ b/.claude/skills/design-system-inheritance/SKILL.md
@@ -0,0 +1,17 @@
+---
+name: design-system-inheritance
+description: Use when building or reviewing UI in a consuming app. Enforces that the app extends FuzeFront's base design system (@fuzefront/design-system) rather than forking it, and that feature code uses only inherited tokens/components.
+---
+
+# design-system-inheritance
+
+The base "fuse seam" design system lives in FuzeFront as `@fuzefront/design-system` (single source for color/spacing/type/primitives). Consuming apps **extend**, never fork.
+
+## Rules
+- The app declares a **local DS package** (`designSystem.extendsAs` in the manifest, e.g. `@fuzex/design-system`) that **imports** base tokens and **composes** base components. It adds app-specific tokens/components; it never redefines a base primitive.
+- Feature code imports from the local DS (which re-exports the base) — **never raw hex/rgb, raw px spacing, or one-off type**.
+- Responsive breakpoints, a11y, and RTL come from the base.
+- Only `frontend-engineer` edits `design-system/`. FuzeFront's frontend-engineer owns the base; each app's owns its extension.
+
+## CI check
+A gate fails the build if feature code (outside the DS package) contains raw design values (`#hex`, `rgb(`, hard-coded `px` spacing, raw font sizes) or imports a base primitive directly instead of via the local DS. Keep the check in the repo's CI; it is part of `sdlc-bootstrap` for apps.
diff --git a/.claude/skills/doc-validity/SKILL.md b/.claude/skills/doc-validity/SKILL.md
new file mode 100644
index 0000000..55463c0
--- /dev/null
+++ b/.claude/skills/doc-validity/SKILL.md
@@ -0,0 +1,10 @@
+---
+name: doc-validity
+description: Use to verify documentation is valid, not aspirational: links resolve, code examples compile/run, and OpenAPI/specs match the docs.
+---
+
+# doc-validity
+
+Checks: dead-link scan; example-code compiles/executes in CI; OpenAPI↔docs drift (generated reference matches the spec); README commands actually work. Owned by docs-maintainer. Flag any doc that cannot be verified against source.
+
+*(Structured stub — flesh out with concrete commands/checks as the first repo adopts it.)*
diff --git a/.claude/skills/feature-flags/SKILL.md b/.claude/skills/feature-flags/SKILL.md
new file mode 100644
index 0000000..5c0d17f
--- /dev/null
+++ b/.claude/skills/feature-flags/SKILL.md
@@ -0,0 +1,95 @@
+---
+name: feature-flags
+description: Use when planning or writing code that should ship behind a feature flag — new/risky work, a gradual rollout, an operational kill-switch, an experiment, or a plan/tenant-gated capability. Covers the flag types and when to use each, the `..` naming, the evaluation-context contract, default-OFF release / default-ON kill-switch rules, testing BOTH states, lifecycle + debt cleanup, and how to read a flag via `@fuzefront/feature-flags` (OpenFeature) in backend and frontend. Owned by feature-flags-engineer.
+---
+
+# Feature flags
+
+Decouple **deploy** from **release**: merge and ship code dark, turn it on deliberately, and kill a bad path without a redeploy. Every flag is a small contract with an owner and an expiry — not a permanent `if`.
+
+## Architecture (the family standard)
+- **Backend:** **Unleash** (self-hosted OSS, **FuzeFront-hosted**) is the flag store + admin UI + rollout/targeting engine.
+- **API:** **OpenFeature** (vendor-neutral SDK) + the **Unleash OpenFeature provider** — you code against the OpenFeature API, so Unleash stays swappable.
+- **Client:** the private **`@fuzefront/feature-flags`** package wraps OpenFeature + the provider with the family's defaults and context contract. Consumers depend on it; they never wire OpenFeature/Unleash by hand.
+- **Ownership:** `feature-flags-engineer` owns the Unleash config, the taxonomy, and the flags. Family products **manage flags through that agent**, not by clicking around Unleash ad hoc. The Unleash *deploy* is `devops-engineer`; the *client package build* is `backend-engineer`.
+
+## When to flag
+Wrap **new or risky** work in a flag — anything you'd want to roll out gradually, turn off fast, measure, or gate by entitlement. Skip flags for trivial, low-risk, irreversible-anyway changes (a typo fix, a pure refactor with tests). When in doubt on a user-facing or money/security-adjacent path, flag it.
+
+## Flag types — pick exactly one
+| Type | Purpose | Default | Lifespan | Removal criterion |
+|---|---|---|---|---|
+| **release** | Ship-dark / gradual rollout of new work | **OFF** | Short (days–weeks) | Removed once 100% rolled out + stable |
+| **ops-kill-switch** | Circuit-breaker for a risky/expensive path | **ON** | Long-lived | Removed only if the path is removed |
+| **experiment** | A/B or multivariate measurement | per design (usually control) | The measurement window | Removed when the experiment concludes + winner is shipped |
+| **permission** | Gate a capability by plan/entitlement/tenant | per entitlement | Long-lived | Removed if the capability becomes universal |
+
+Rules that are NOT negotiable:
+- **release ⇒ default OFF.** A release flag that defaults ON has shipped the feature — pointless.
+- **ops-kill-switch ⇒ default ON.** The safe path is "system works"; flipping OFF is the break-glass.
+- **permission flags are rollout convenience, NOT authorization.** Real entitlement is enforced by **Permit** (`permit.check`) on the server. A flag may *also* hide the UI/route, but it must never be the only thing standing between a user and a capability — that's a BOLA waiting to happen.
+
+## Naming — `..`
+Dot-namespaced, lowercase, kebab within a segment: `fuzefront.billing.usage-based-pricing`, `fuzekeys.tokenizer.batch-detokenize`, `fuzefront.checkout.new-cart-kill-switch`. The `` prefix prevents collisions across the family in one shared Unleash; `` groups by service/area; `` is the specific toggle. Don't encode the type in the name beyond what's natural (a `-kill-switch` suffix is fine and readable).
+
+## Evaluation context contract
+Every evaluation passes a context so Unleash can target correctly. The family-standard fields (set by `@fuzefront/feature-flags`):
+- **`environment`** — `local` | `dev` | `prod` (the Unleash environment; usually injected from config, not per-call).
+- **`organizationId` / `tenantId`** — the org/tenant the request acts on (drives per-tenant rollout + permission flags).
+- **`userId`** — the acting user (drives gradual-by-user rollout, experiment bucketing, stickiness).
+- **`app`** — the consuming app/service id (e.g. `fuzefront-host`, `billing-service`).
+
+**Never evaluate with no context in a prod path** (you'd get only the default and lose targeting). If you genuinely have no user/tenant (a cron, a system task), pass `app` + `environment` and document why. Context flows from the request: backend reads it from the authenticated principal + headers; frontend reads it from the session/host shell.
+
+## Reading a flag via `@fuzefront/feature-flags` (OpenFeature)
+
+**Backend (server SDK)** — evaluate per-request with the request's context; never cache a boolean across users:
+```ts
+import { getClient } from '@fuzefront/feature-flags'; // wraps OpenFeature + Unleash provider
+
+const flags = getClient();
+const ctx = { environment: env, organizationId: req.org.id, userId: req.user.id, app: 'billing-service' };
+// release flag: default OFF
+if (await flags.getBooleanValue('fuzefront.billing.usage-based-pricing', false, ctx)) {
+ // new path
+}
+// kill-switch: default ON — code runs unless explicitly killed
+if (await flags.getBooleanValue('fuzefront.checkout.charge-kill-switch', true, ctx)) {
+ await charge();
+}
+```
+
+**Frontend (web/proxy SDK)** — the browser uses the Unleash **proxy/frontend** token (never the server admin token); context comes from the host session:
+```ts
+import { useFlag } from '@fuzefront/feature-flags/react';
+const showNewCart = useFlag('fuzefront.checkout.new-cart', false); // default OFF
+```
+The default value passed in code (the 2nd arg) is the **fallback when the flag store is unreachable** — make it match the type rule (OFF for release, ON for kill-switch) so an Unleash outage fails safe.
+
+## Testing — BOTH states, always
+A flagged change has **two** code paths; CI must exercise both. Don't test only the on-path.
+- Unit/integration: run the suite with the flag **OFF** (the old/safe path) **and ON** (the new path). The `@fuzefront/feature-flags` client exposes a test provider (static/in-memory) so a test pins a flag's value — no network, deterministic.
+- For a kill-switch, test that flipping OFF cleanly disables the path (no half-state, no thrown 500).
+- For permission flags, also assert the **Permit** check still gates the capability with the flag ON (the flag is not the boundary).
+
+## Lifecycle + debt cleanup
+A flag is **debt** the moment it's created. At creation, record (in Unleash + the flag's PR): **owner**, **type**, **default**, and **removal criterion**. Then:
+1. **release / experiment flags are temporary** — once rolled out (or the experiment concludes), delete the flag AND the dead branch in code, in one cleanup PR. A long-lived release flag is a smell.
+2. **Stale-flag sweep** — flags past their removal criterion (or untouched > their expected lifespan) are surfaced by the governance reconciliation sweep and the flag owner is nudged. Don't let the codebase fill with permanently-ON `if (true)` flags.
+3. **Removing a flag** = delete the toggle in Unleash + remove both branches in code (keep the winning path) + drop the test for the dead path. Verify nothing else references the flag key first.
+
+## Consuming-repo onboarding (point a repo at the family flag service)
+1. Add `@fuzefront/feature-flags` as a dependency (private GitHub Packages, `@fuzefront` scope — scoped `.npmrc` + token).
+2. Get a **client token** for your app from `feature-flags-engineer` (a scoped Unleash API token — server token for backend, frontend/proxy token for browser). It's a SealedSecret in your repo, ref'd by env; `devops-engineer` wires it.
+3. Point the provider at **FuzeFront's Unleash** URL (same-origin proxy in the browser; service-DNS for server-side) via config — never hard-code the host.
+4. Pass the standard evaluation context (above) on every evaluation.
+5. Use the `..` namespace; ask `feature-flags-engineer` to create the flags (with type + owner + removal criterion).
+
+## Done checklist
+- [ ] Flag has exactly one **type**; default matches the rule (release OFF, kill-switch ON)
+- [ ] Name is `..`
+- [ ] Evaluation passes the standard **context** (environment + org/tenant + user + app) on prod paths
+- [ ] In-code default is the fail-safe value (OFF release / ON kill-switch)
+- [ ] **Both** flag states tested (off-path AND on-path); permission flags still gated by Permit
+- [ ] Flag recorded with **owner + removal criterion**; release/experiment flags scheduled for cleanup
+- [ ] Read via `@fuzefront/feature-flags` (OpenFeature API), not a hand-wired Unleash/OpenFeature call
diff --git a/.claude/skills/feature-tech-planning/SKILL.md b/.claude/skills/feature-tech-planning/SKILL.md
new file mode 100644
index 0000000..22e364b
--- /dev/null
+++ b/.claude/skills/feature-tech-planning/SKILL.md
@@ -0,0 +1,65 @@
+---
+name: feature-tech-planning
+description: Use when planning or designing ANY new feature, component, or service, before implementation. Researches existing libraries/services that could implement the capability, presents an honest tradeoff comparison with a clear build-vs-adopt recommendation, and reviews/optimizes the design for componentized architecture (reusable npm packages + standalone microservices). Part of the standard SDLC — apply it during brainstorming/planning, not after.
+---
+
+# Feature Tech Planning (library research + componentized architecture)
+
+Before designing how to BUILD a capability, establish whether it should be built at all, what to build it ON, and how to package it. Skipping this leads to reinventing solved problems and to monoliths that can't be reused.
+
+## When to use
+Any time you are planning a feature, component, service, or non-trivial capability — during brainstorming/writing-plans, before implementation. Pairs with `superpowers:brainstorming` and `superpowers:writing-plans`.
+
+## The process
+
+### 1. Name the capability precisely
+State the capability in one line and its hard requirements (functional + non-functional: scale, latency, compliance, multi-tenancy, offline, license constraints). Requirements drive the evaluation.
+
+### 2. Research existing solutions — don't reinvent
+Actively look for libraries, SDKs, and managed services that already solve this (npm, GitHub, the relevant ecosystem, the vendor's own SDK). Prefer a well-maintained existing solution over bespoke code for any non-differentiating capability. Use web search / docs for current state — do not rely on memory for versions, maintenance status, or pricing.
+
+### 3. Build an honest tradeoff comparison
+For the top 2–4 candidates (including the "build it ourselves" option), compare on the axes that matter for THIS requirement, e.g.:
+- **Fit** — does it actually cover the requirements, or only 70%?
+- **Maturity & maintenance** — release cadence, last release, open issues, who backs it.
+- **License** — MIT/Apache vs copyleft vs commercial; redistribution implications.
+- **Footprint** — bundle size (frontend), dependencies, runtime weight.
+- **DX / types** — TypeScript support, docs quality, API ergonomics.
+- **Lock-in & exit cost** — how hard to migrate off; proprietary data formats; managed-service dependency.
+- **Security & compliance** — track record, SOC2/PCI/etc. where relevant.
+Present it as a compact table. Be honest about the downsides of your recommended option.
+
+### 4. Recommend — build vs adopt vs buy
+Give ONE clear recommendation with reasoning, and name the runner-up and when you'd switch to it. "Build" is the right call only for genuine differentiators or where no option fits; say so explicitly when you choose it.
+
+### 5. Componentized-architecture review (mandatory)
+Always evaluate whether the capability should be a **reusable, independently-versioned unit** rather than woven into an app:
+- **Reusable npm package** for shared frontend components, SDKs, clients, schemas, and cross-cutting logic. Define the public interface and what stays internal.
+- **Standalone microservice** (also published as an npm client package) for backend capabilities that have their own lifecycle, scaling, or deployment boundary.
+- Default toward extraction for anything used by >1 consumer or that has an independent lifecycle. Avoid premature splitting of things that are genuinely one unit (YAGNI) — but justify keeping something inline.
+- Name the package/service boundary, its public API, and its dependencies. Files/things that change together live together.
+- **Private publishing (mandatory).** Reusable npm packages publish to the project's **private** registry with `access: restricted` — never public npm. Each package's `package.json` carries a `publishConfig` (registry + `access: restricted`) and a `repository` field, and is wired into the release pipeline. For FuzeFront the registry is **GitHub Packages** (`https://npm.pkg.github.com`) under the `@fuzefront` scope; a scoped `.npmrc` (`@fuzefront:registry=...` + `GITHUB_TOKEN`/PAT auth) governs install + publish. A package is not "done" until its private publish-config is set.
+
+### 5b. UI work is design-system-first (every component, no exceptions)
+Maintaining the design system's UI/UX is part of building ANY UI component. Plan components against the project's design system BEFORE coding: use the design-system / frontend-design skill, extend the design system's tokens/components rather than one-off styling (if a needed component is missing, add it to the system instead of bypassing it), and produce concrete component specs (states, variants, tokens, a11y) to hand to implementing agents. Never hand UI to coders without a design-system-aligned spec. Each UI task's "done" includes a conformance check: no hard-coded colors/spacing/type outside the tokens, components reused not reinvented.
+
+### 5c. Deploy wiring is part of "done" (GitOps / Argo CD)
+A microservice or component is not complete until it is deployable through the project's GitOps pipeline. For any new service/package, the plan MUST include its deploy wiring: Helm Deployment+Service+values (gated by an `enabled` flag), its image in the release/CI build matrix + the prod values tag-bump, and the Argo CD wiring that syncs it. Prefer the established structure — for FuzeFront: **hybrid Argo** — core/coupled services live in the umbrella `fuzefront` chart synced by one Argo Application; independently-lifecycled services (e.g. billing, chat, the LLM gateway) get their **own Argo Application**. Prod is GitOps (Argo syncs from git; never hand-deploy to prod); local is Helm/Skaffold on kind.
+
+### 6. Surface it before implementing
+Put a short **"Library & Architecture Review"** section in the spec/plan: the comparison table, the recommendation + reasoning, and the package/service boundaries. Get the human's call on build-vs-adopt and on any managed-service/lock-in tradeoff before writing implementation code.
+
+## Red flags
+- Hand-rolling something a mature library/managed service already does well (auth, payments, OTP, RAG plumbing, rate-limiting, date math…).
+- Designing a capability directly inside an app when a second consumer already exists or is obviously coming → it should be a package.
+- A backend feature with its own lifecycle/scaling glued into the monolith → it should be a service.
+- Choosing a library without naming its downsides, license, or exit cost.
+- Recommending "build" without justifying why no existing option fits.
+
+## Output checklist
+- [ ] Capability + hard requirements stated
+- [ ] Existing libraries/services researched (current data, not memory)
+- [ ] Tradeoff table for 2–4 candidates incl. "build"
+- [ ] One recommendation + reasoning + runner-up
+- [ ] Componentization decision: npm package(s) / microservice boundaries named, with public interfaces
+- [ ] "Library & Architecture Review" section added to the plan; human consulted on build-vs-adopt + lock-in
diff --git a/.claude/skills/fuzefront-ui-package/SKILL.md b/.claude/skills/fuzefront-ui-package/SKILL.md
new file mode 100644
index 0000000..6cbb795
--- /dev/null
+++ b/.claude/skills/fuzefront-ui-package/SKILL.md
@@ -0,0 +1,34 @@
+---
+name: fuzefront-ui-package
+description: Use when building ANY FuzeFront frontend UI (a feature's UI package, a new component, or a screen). Enforces the non-negotiables — ship it as a private npm package, design-system-first (extend the design system, never one-off styles), RTL/LTR + a11y, build against the API contract/client, and update the design system via its own skill when a component is missing.
+---
+
+# FuzeFront UI package
+
+Every piece of FuzeFront UI ships as a reusable, design-system-first npm package — never inline one-off styling, never ad-hoc components.
+
+## Non-negotiables (each is part of "done")
+
+1. **Ship as a private npm package.** `packages/` → `@fuzefront/`, with `publishConfig` (registry `https://npm.pkg.github.com`, `@fuzefront` scope, `access: restricted`) + a `repository` field, wired into the lerna/release publish pipeline. Dual build (es/cjs + `.d.ts`). It's not done until the private publish-config is set.
+2. **Design-system-first — extend, don't bypass.** Build ONLY from `@fuzefront/design-system` ("fuse seam") components + CSS-variable tokens (`--bg-*`, `--text-*`, `--accent-*`, `--space-*`, `--radius-*`, `--font-*`, `--seam`, …). **Zero hard-coded colors/spacing/type** — a hex/rgba/px-literal in component code is a defect.
+3. **If a needed component/token is missing from the design system, ADD it to the design system** (using the design-system / `frontend-design` skill) — do NOT one-off it in the feature package. The design system stays the single source of truth; reuse before you create.
+4. **Build against the API contract/client.** Consume the generated `@fuzefront/-client` types (see `api-contract-first`); never hand-write request/response shapes. Develop against the contract mock server so you don't wait on the backend.
+5. **RTL/LTR + a11y.** Use CSS **logical properties** (`margin-inline`, `padding-inline-start`, `inset-inline`) so components mirror automatically; consume `@fuzefront/i18n` for strings + direction. Labels/roles/keyboard nav + visible focus (fuse-seam ring) on every interactive element.
+6. **Test (TDD).** Vitest unit tests for logic + render + a11y + an RTL flip; type-check + library build green. (Full-stack Playwright e2e is a separate, live-stack concern.)
+7. **Resolve in-repo packages from source, never from the registry.** When the host app (or any consumer) imports a new `@fuzefront/*` package that lives in this monorepo, wire it **exactly** like `@fuzefront/design-system` is already wired — workspace/source resolution (`workspace:*`, or the root workspace + vite/vitest/tsconfig alias) — **never a fetchable semver like `"^0.1.0"`**. A `@fuzefront/*` dependency with a numeric version and no published artifact is a build break, not a version pin (it 404s on `npm ci` — this was the PR #65 failure). Prove it with a clean `npm ci` in the **Linux/Docker** path (the `os=linux` npmrc gotcha hides this on Windows): **zero registry 404s before claiming green.** This is enforced by the **required** `In-repo packages resolve from source` CI gate (`scripts/check-workspace-deps.mjs`).
+8. **Type-check against built `.d.ts`, bundle from source (PR #65 finish-pass lessons).** The `frontend` host is NOT a root workspace — it has its own `node_modules`, so compiling a UI package's *source* under the host's `tsc` pulls in a **second `@types/react`/`csstype`** copy → `CSSProperties` TS2322 clashes. Therefore: in `frontend/tsconfig.json`, resolve each consumed `@fuzefront/*` UI package to its built **`dist/index.d.ts`** (like `@fuzefront/design-system`) and **build it in CI before the frontend type-check**; keep the vite/vitest aliases pointing at **source** for bundling/tests. Also: the Module-Federation `shared` array stays **`['react','react-dom']` only** — never list `@fuzefront/*` packages there (they are source-file-aliased, so the plugin reads `/package.json` → `ENOTDIR` and breaks `vite build`); host UI packages are bundled directly. And a `tsc`/build job will surface **undeclared transitive deps** (e.g. `socket.io-client` imported in `shared` but never declared) — declare them in the owning `package.json`.
+
+## Procedure
+1. Plan the components against the design system FIRST (states, variants, tokens, a11y) — if a primitive is missing, extend the design system via its skill, in the design-system package, before building the feature UI.
+2. Scaffold `packages/` with the private `publishConfig` + build setup.
+3. Implement design-system-first against the `@fuzefront/-client` + `@fuzefront/i18n`; TDD.
+4. Verify: vitest green, type-check clean, library build (es/cjs/dts), no hard-coded design values (grep for hex/rgba), a11y assertions pass.
+5. Wire into the frontend container (mount + Module-Federation `shared` entry) **resolving the package from source like `@fuzefront/design-system` — never a registry semver** (non-negotiable 7); confirm a clean `npm ci` in Docker has zero 404s; keep the PR draft until verified.
+
+## Done checklist
+- [ ] `@fuzefront/` private publishConfig + repository + in publish pipeline
+- [ ] only `@fuzefront/design-system` tokens/components; zero hard-coded color/spacing/type
+- [ ] missing primitives added to the design system (not one-offed)
+- [ ] consumes the generated `@fuzefront/-client` + `@fuzefront/i18n`; RTL via logical properties; a11y
+- [ ] vitest + type-check + dual build green
+- [ ] new `@fuzefront/*` consumer dep resolved from source (workspace/alias), not a registry semver; clean `npm ci` in Docker has zero 404s
diff --git a/.claude/skills/logging/SKILL.md b/.claude/skills/logging/SKILL.md
new file mode 100644
index 0000000..055b334
--- /dev/null
+++ b/.claude/skills/logging/SKILL.md
@@ -0,0 +1,168 @@
+---
+name: logging
+description: Use when writing or reviewing ANY service/backend/frontend code that runs a request path, calls an external system, or makes a decision that could fail in prod — the family structured-logging standard. Covers pino JSON logging, the ERROR/WARN/INFO/DEBUG level semantics + env-flippable LOG_LEVEL, boundary logging (every external call logs start/end/elapsed ms), per-request correlation IDs (reqId child logger), MANDATORY secret redaction, critical-path-first incremental migration off raw console.*, and the implementer + reviewer definition-of-done. Owned by backend-engineer/frontend-engineer (implement) + the reviewer (enforce).
+---
+
+# logging / observability-logging
+
+Structured, leveled, correlated logging on every critical path. **Silent code is undebuggable in prod** — this skill is the family standard for making sure it never ships that way.
+
+## Why this is a hard rule (the incident that wrote it)
+
+A production login **hung**, and it cost hours to diagnose because FuzeFront's security service had **no structured logging** — 210 raw `console.*` calls and the auth-critical files (the OIDC authorize/callback hairpin) were **completely silent**. There was nothing to grep, no timing, no correlation. Once per-step timing logs were added at the boundaries, the root cause — a **6.5s authorize hairpin** — was found in **30 seconds**.
+
+The lesson, encoded here as policy: **leveled + structured + correlated logging on critical paths is not optional.** A path with no logs is a path you cannot debug in prod; when it breaks (and auth/payment paths break at the worst time), you are blind. Instrument first, especially at boundaries and decision branches.
+
+## 1. Structured JSON via pino — one shared logger per service
+
+- **Family standard: [`pino`](https://getpino.io)** for Node/TypeScript services. Structured **JSON** to stdout — the platform's log stack (Loki) ingests JSON; free-text `console.log` is not queryable.
+- **One shared logger util per service** (`src/lib/logger.ts` or equivalent). Never `new` a logger ad hoc per file, and never `console.*` in service code. Everything derives from the one root logger (child loggers, below).
+- **Never `console.log`/`console.error` in new service code.** `console.*` is unleveled (can't be filtered), unstructured (can't be queried), and unredacted (leaks secrets). The gate for new code is zero raw `console.*`.
+
+## 2. Levels + `LOG_LEVEL` — flip verbosity WITHOUT a redeploy
+
+Verbosity is controlled by the **`LOG_LEVEL` env var**, read at startup. **Prod default: `info`.** To debug a live incident you set `LOG_LEVEL=debug` and restart the pod (or use a runtime level endpoint) — **no code change, no rebuild, no redeploy**. This is the whole point: the debug detail is already *written into the code*, dormant at `info`, and you turn it on when you need it.
+
+| Level | Semantics | Examples |
+|-------|-----------|----------|
+| **ERROR** | An operation failed; needs attention. Always logged **with context** (see §3). | unhandled exception, external call failed after retries, invariant violated, auth misconfig |
+| **WARN** | Degraded / recoverable / suspicious, but handled. | retry succeeded on 2nd attempt, fell back to cache, deprecated path hit, slow dependency over threshold |
+| **INFO** | **Flow milestones** — the request-level story a human follows. Prod-safe volume. | request received, auth succeeded for user X, order created, external call *initiated* (summary), request completed |
+| **DEBUG** | **Per-hop detail** — every boundary crossing incl. **elapsed ms**, decision-branch inputs/outcomes, intermediate state. Off in prod by default. | `authorize() start`, `authorize() end elapsed=6512ms`, "chose branch B because hasPassword=null", DB query issued/returned rows=N |
+
+Rule of thumb: **INFO tells the story, DEBUG tells the timing and the why.** If you'd want it during a 2am incident but not in steady-state prod noise, it's DEBUG.
+
+## 3. Boundary logging — the rule that finds the 6.5s hairpin
+
+**Every external call logs start + end + elapsed ms.** External = any hop out of the process: HTTP/fetch to another service or provider (Authentik, Permit, Stripe, Unleash), a DB query, a cache/Redis op, a Kafka produce/consume, a filesystem/network call. This is what turned "the login hangs somewhere" into "authorize() took 6512ms" in 30s.
+
+```
+DEBUG authentik.authorize start { reqId, op: "authorize" }
+DEBUG authentik.authorize end { reqId, op: "authorize", elapsedMs: 6512, status: 200 }
+```
+
+- **Every caught error logs WITH context** — never `catch {}` (a swallowed error is a silent failure) and never `catch(e){ throw e }` with nothing logged. Log the error object, the operation, and the identifying context (`reqId`, `userId`/`orgId`, the resource id, the upstream status). An error with no context is nearly as useless as no error.
+- **Critical-path decision branches are observable.** Where the code chooses a path on auth/payment/data flows, log the input that drove it and which branch was taken — including the **fail-closed** cases (reveal-once token already revealed; remove-last-2FA-factor → 409; demote-the-last-admin; `hasPassword: null` → "set a password first"). If prod took the wrong branch, the log must let you see *why*.
+
+## 4. Correlation IDs — one request, one greppable thread
+
+Every request gets a **`reqId`** (accept an inbound `x-request-id` / `traceparent` if present, else generate a UUID) bound as a **child-logger** field at the entry middleware, and that child logger is used for the rest of the request:
+
+```ts
+const reqId = req.headers['x-request-id'] ?? randomUUID();
+req.log = logger.child({ reqId });
+```
+
+- **One request = one greppable thread.** `grep reqId=abc123` returns the entire lifecycle across every hop in order. This is the difference between reading a story and staring at interleaved noise.
+- **Carry the correlation id across hops** — propagate `reqId` as an outbound header (`x-request-id`) on every external call so the downstream service's logs join the same thread. Bind stable request-scoped context too (`userId`, `orgId`/tenant, route) once on the child so you don't repeat it on every line.
+
+## 5. Secret redaction — MANDATORY, non-negotiable
+
+**Never log a credential.** Passwords, tokens (access/refresh/ID), auth codes, OTPs, cookies, `client_secret`, API keys, and `Authorization` headers are **redacted at the logger** via pino's `redact` option — you do not rely on remembering to omit them at each call site. A logged credential is a **security incident** (it lands in Loki, backups, screenshots) and, on auth/payment code, is a review **blocker**.
+
+```ts
+redact: {
+ paths: [
+ 'req.headers.authorization', 'req.headers.cookie',
+ '*.password', '*.token', '*.access_token', '*.refresh_token', '*.id_token',
+ '*.code', '*.otp', '*.client_secret', '*.apiKey', '*.secret',
+ 'password', 'token', 'authorization', 'cookie', 'set-cookie',
+ ],
+ censor: '[REDACTED]',
+}
+```
+
+Hard rule for auth/payment code: **if in doubt, redact.** Log the *shape* (`tokenLength`, `hasPassword: boolean`, `codePresent: true`), never the value.
+
+## 6. Critical-path-first + incremental migration
+
+You do **not** need a big-bang rewrite to adopt this. Order the work by blast radius:
+
+1. **Instrument the critical paths first** — auth, payments, and data-mutation flows. These are where prod blindness hurts most (the incident was auth). Add boundary logs + reqId + redaction here before anything else.
+2. **Migrate legacy `console.*` incrementally** — replace raw `console.*` with the shared leveled logger opportunistically as you touch files; a standing cleanup ticket tracks the long tail (e.g. the 210 calls). It does not block feature work, but it does get done.
+3. **No raw `console.*` in NEW code — ever.** New code is held to the full standard from line one; that's the ratchet that stops the debt from growing while the legacy tail shrinks.
+
+**Frontend note:** the same principles apply in the browser — a shared leveled logger (pino works in the browser, or a thin wrapper), no stray `console.log` in shipped code, redact before logging user data, and correlate client errors with a request id where one exists. Browser console-cleanliness at runtime is separately gated by `ui-runtime-validation`; this skill is about *deliberate* app logging, not leftover debug noise.
+
+## 7. Definition of done — implementer + reviewer
+
+**Owners:** `backend-engineer` and `frontend-engineer` **implement** this on the code they write; the **reviewer** (`appsec-reviewer` on endpoint/auth PRs, or the orchestrator's review pass) **enforces** it — a critical path that ships silent, or a credential that reaches a log, is a **REPORTED finding / blocker**, not something the reviewer patches.
+
+**Implementer checklist (before `SCOPE DONE`):**
+- [ ] Uses the one shared pino logger; **zero raw `console.*`** in the code I added.
+- [ ] `LOG_LEVEL` read from env; prod paths sane at `info`, rich detail at `debug` (no redeploy needed to get it).
+- [ ] Every external call I added logs **start + end + elapsed ms**; every `catch` logs the error **with context**; critical decision branches (incl. fail-closed cases) are observable.
+- [ ] Requests carry a **`reqId`** child-logger binding, propagated to downstream calls.
+- [ ] pino **`redact`** covers passwords/tokens/codes/cookies/`client_secret`/`authorization`; I logged no credential values (shape only).
+- [ ] Critical paths (auth/payment/data) instrumented first; any legacy `console.*` I couldn't migrate is captured in a cleanup ticket.
+
+**Reviewer checklist (enforce, don't fix):**
+- [ ] No new critical path ships **silent** (no boundary/decision logs) — REPORT if so.
+- [ ] No credential can reach a log (redact paths cover the call sites) — **blocker** on auth/payment code.
+- [ ] Levels used correctly (errors at ERROR with context; not everything at INFO; debug detail actually at DEBUG).
+- [ ] `reqId` correlation present and propagated.
+
+## 8. Reference — copy-paste TS
+
+```ts
+// src/lib/logger.ts — the ONE shared logger per service
+import pino from 'pino';
+
+export const logger = pino({
+ level: process.env.LOG_LEVEL ?? 'info', // prod default info; flip to debug via env, no redeploy
+ redact: {
+ paths: [
+ 'req.headers.authorization', 'req.headers.cookie', 'res.headers["set-cookie"]',
+ '*.password', '*.token', '*.access_token', '*.refresh_token', '*.id_token',
+ '*.code', '*.otp', '*.client_secret', '*.apiKey', '*.secret',
+ 'password', 'token', 'authorization', 'cookie',
+ ],
+ censor: '[REDACTED]',
+ },
+ base: { service: process.env.SERVICE_NAME ?? 'unknown' },
+ timestamp: pino.stdTimeFunctions.isoTime,
+});
+
+// request middleware — bind a per-request child logger with a correlation id
+import { randomUUID } from 'crypto';
+export function requestLogger(req, res, next) {
+ const reqId = (req.headers['x-request-id'] as string) ?? randomUUID();
+ req.reqId = reqId;
+ req.log = logger.child({ reqId, route: req.path, method: req.method });
+ req.log.info('request received');
+ res.on('finish', () =>
+ req.log.info({ status: res.statusCode }, 'request completed'));
+ next();
+}
+
+// boundary helper — every external call logs start + end + elapsed ms
+export async function timed(log: pino.Logger, op: string, fn: () => Promise): Promise {
+ const start = performance.now();
+ log.debug({ op }, `${op} start`);
+ try {
+ const out = await fn();
+ log.debug({ op, elapsedMs: Math.round(performance.now() - start) }, `${op} end`);
+ return out;
+ } catch (err) {
+ // caught error ALWAYS logs with context
+ log.error({ op, elapsedMs: Math.round(performance.now() - start), err }, `${op} failed`);
+ throw err;
+ }
+}
+
+// usage on a critical path — this is what would have found the 6.5s hairpin in 30s
+// propagate reqId downstream so the next service's logs join the same thread
+const tokens = await timed(req.log, 'authentik.authorize', () =>
+ fetch(authorizeUrl, { headers: { 'x-request-id': req.reqId } }));
+// decision branch, observable + fail-closed reason logged (no secret value)
+if (user.hasPassword === null) {
+ req.log.info({ userId: user.id }, 'blocked: user must set a password first');
+ return res.status(409).json({ error: 'set a password first' });
+}
+```
+
+## Related
+- `ui-runtime-validation` — browser **console-cleanliness** at runtime (leftover errors/noise), complementary to deliberate app logging.
+- `endpoint-authorization` — auth/authz on endpoints; boundary logging + redaction are strongest exactly on these paths.
+- `verification-protocol` — the honest scoped-done discipline this skill's DoD plugs into.
+- Baseline §7.2 (Observability — structured logging) is the L0 statement of this policy.
diff --git a/.claude/skills/managed-agents-roles/SKILL.md b/.claude/skills/managed-agents-roles/SKILL.md
new file mode 100644
index 0000000..07961d6
--- /dev/null
+++ b/.claude/skills/managed-agents-roles/SKILL.md
@@ -0,0 +1,92 @@
+---
+name: managed-agents-roles
+description: Use to define, provision, and maintain a repo's Managed-Agents roles (agent-templates/) — persona→role.json→environment→vault→provision, on the provider-abstracted framework. The framework is canonical in FuzeSDLC and stamped by sdlc-bootstrap; this skill covers authoring the repo's own concrete roles/environments and wiring provisioning.
+---
+
+# managed-agents-roles
+
+Projects the org's persona agents (`.claude/agents/*.md`) onto a provider's **managed-agents**
+runtime so a coding session never stalls asking a human to "run this on the cluster" or "do
+this on GitHub". A **role** = persona (system) + environment (packages + network reach) +
+permissions (per-tool `always_allow`/`always_ask` + vault-scoped creds).
+
+Read `agent-templates/README.md` first — it explains the split: the **framework**
+(`schema/`, `roles/_base/`, `sync/`, `providers/`) is canonical in FuzeSDLC and kept current
+by `governance-sync`; the **concrete** definitions below are the repo's own; the deployed
+**orchestration runtime** (handoff MCP, worker) lives in FuzeAgent.
+
+## When to use
+
+- Adding a managed-agents role to a repo (or the first one — onboarding the `roles` axis).
+- Changing a role's tools/policies/MCP servers/environment, or a persona that drives it.
+- Wiring provisioning (the `provision-sync.yml` caller + the secrets it needs).
+
+## Define a role
+
+1. **Persona** — ensure `.claude/agents/.md` exists (a canonical agent from
+ FuzeSDLC, installed by `sdlc-bootstrap`, or an authored repo-local one). Its body (YAML
+ frontmatter stripped) becomes the agent `system`. Never duplicate the persona into the
+ role JSON — reference it.
+2. **role.json** — `agent-templates/roles//role.json`:
+ ```json
+ {
+ "$schema": "../../schema/role-manifest.schema.json",
+ "role": "",
+ "extends": "_base",
+ "name": "",
+ "persona": ".claude/agents/.md",
+ "environment": "",
+ "system_append": "role-specific guardrails (added to _base, never replacing it)",
+ "tools": [ /* extra mcp_toolset entries + per-tool permission_policy */ ],
+ "mcp_servers": [ /* { "type":"url","name":"...","url":"${SOME_MCP_URL}" } */ ],
+ "skills": [],
+ "services": { "github": "write", "k8s": "none", "cloud": "none" }
+ }
+ ```
+ `_base` already supplies the guardrail system prompt, the `agent_toolset` + `github` +
+ `handoff` MCP tools, and their `always_allow` policies. Override per key; `system_append`
+ and `services` are merged, not replaced.
+3. **Environment** — `agent-templates/environments/.json` (`schema/environment.schema.json`).
+ `cloud-*` = a provider sandbox (packages + allowed network); `selfhosted-*` = a queue drained
+ by a worker inside your network that holds the real creds. Environments are **not versioned**
+ — the adapter archives+recreates by name on config change. Bind a repo to a role at launch via
+ the environment (that is the per-repo axis — one role, many environments).
+4. **Vault** (optional) — `agent-templates/vaults/.json` (`schema/vault.schema.json`) for
+ MCP-auth / channel creds; reference `${VAR}` — never inline a secret. Creds with an empty/unset
+ token are skipped at provision time.
+5. **Guardrails** — production actions (`kubectl`/`helm`/`terraform` writes, money, outbound
+ customer messages) must be `always_ask`; a self-hosted worker adds OS-level guard shims + a
+ scoped-RBAC kubeconfig (defense-in-depth, not the primary boundary).
+
+## Declare + provision
+
+6. **Manifest** — add the `roles` block to `.fuze/manifest.json`
+ (`governance/repo-manifest.schema.json`):
+ ```json
+ "roles": { "source": "agent-templates/", "runtime": "managed-agents", "provider": "anthropic",
+ "defined": ["backend","frontend","devops"], "environments": ["cloud-backend","selfhosted-devops"] }
+ ```
+ Its presence tells `sdlc-bootstrap` to stamp the `provision-sync.yml` caller.
+7. **Validate + preview** (offline, no API calls):
+ ```bash
+ cd agent-templates && python sync/validate.py && python providers/provision.py --provider anthropic --dry-run
+ ```
+8. **Provision** — merging a definition change to `main` triggers `provision-sync.yml`
+ (→ FuzeSDLC reusable `provision.yml`), which reconciles agents/environments/vaults/memory
+ into their deployed counterparts. Needs repo secrets `MANAGED_AGENTS_API_KEY` (Console key
+ with the `managed-agents` beta — NOT a Claude Code token) + the referenced `*_MCP_URL`s.
+ Absent key → the job skips (never fails a merge).
+
+## Maintain
+
+- The framework files (`schema/`, `roles/_base/`, `sync/`, `providers/`) are canonical in
+ FuzeSDLC — do **not** hand-edit them per repo; `governance-sync` will revert drift. Change the
+ framework in FuzeSDLC; repos pick it up on their next PR.
+- Editing a persona or a `role.json` re-syncs that agent on the next merge. Adding a role
+ creates it. Provisioning never prunes — archiving an orphaned agent is a separate step.
+
+## Verify
+
+`python sync/validate.py` passes; `provision.py --dry-run` lists the expected roles/envs; after
+merge, the provisioned agents/environments are visible in the provider console; `governance-sync`
+reports the framework in sync.
diff --git a/.claude/skills/mobile-conformance/SKILL.md b/.claude/skills/mobile-conformance/SKILL.md
new file mode 100644
index 0000000..f0df5ec
--- /dev/null
+++ b/.claude/skills/mobile-conformance/SKILL.md
@@ -0,0 +1,10 @@
+---
+name: mobile-conformance
+description: Use to verify responsive/mobile-layout conformance via device-emulation e2e.
+---
+
+# mobile-conformance
+
+Playwright device profiles + viewport matrix; checks: no horizontal overflow, tap-target sizes, breakpoint correctness, safe-area insets, orientation. Files bugs in Jira. Owned by frontend-test-engineer.
+
+*(Structured stub — flesh out with concrete commands/checks as the first repo adopts it.)*
diff --git a/.claude/skills/model-cascade/SKILL.md b/.claude/skills/model-cascade/SKILL.md
new file mode 100644
index 0000000..88b489a
--- /dev/null
+++ b/.claude/skills/model-cascade/SKILL.md
@@ -0,0 +1,106 @@
+---
+name: model-cascade
+description: Use when executing large-scoped work (multi-file / multi-slice / epic / audit / migration, or anything needing a plan) to run it through the tiered cascade — Opus decomposes into a task tree with per-node tiers, each leaf runs on the cheapest adequate tier (Haiku mechanical / Sonnet scoped / Opus judgment), tiers ESCALATE up rather than guess, parents verify children, and a fresh Opus pass gives the PASS/GAPS/FAIL completeness verdict. Skip it for small atomic tasks (run single-tier). Policy: governance/model-cascade.md.
+---
+
+# model-cascade
+
+The executable protocol for **capability-routed tiered execution**. Run large-scoped work through it; run small atomic tasks single-tier (no decomposition). Full policy + rubric: `governance/model-cascade.md`.
+
+> **Tier is HOW, scope is WHO — orthogonal.** Choosing a tier never changes who owns the deliverable. A domain agent keeps its scope boundary (`routing.md`) regardless of which model runs the call.
+
+## The procedure — decompose → delegate → assemble → verdict
+
+1. **Decompose (Opus).** Turn the work into a **task tree**. For each node, write a **hand-down spec** (template below) and assign a tier via the decision checklist.
+2. **Delegate (down the tiers).** Run each node at its tier. Sonnet may sub-decompose mechanical leaves to Haiku. A node beyond its tier returns `ESCALATE: ` and is re-run **one tier up**.
+3. **Assemble (bottom-up).** Each parent integrates its children and **verifies their output against the spec it handed down**. A child never self-certifies; a lower tier never grades a higher tier's work. Integration conflicts escalate up.
+4. **Verdict (Opus, fresh context).** Grade the assembled whole against the **original** scope → **PASS / GAPS(list) / FAIL**. Separate from the CI Harden Gate and the QA lanes — an additional orchestration gate. Only the orchestrator declares the *feature* done.
+
+## The hand-down spec template (the micro-contract for every node)
+
+```
+NODE:
+- Objective:
+- Inputs:
+- Constraints:
+- Acceptance criteria:
+- Machine-check:
+- Tier: (per the decision checklist)
+```
+
+A node is not ready to delegate until every field is filled. If the spec would be as long as just doing the task, **don't decompose** — run it on the resident tier (anti-over-cascade).
+
+## The decision checklist (route to the lowest adequate tier)
+
+Pick the **lowest** tier whose gate is fully satisfied; re-evaluate on escalation.
+
+- **Haiku** only if **ALL**: fully specifiable (zero judgment) · self-contained (one file / small known set, no cross-system inference) · mechanical/pattern-application · machine-checkable · **bounded blast radius** (never security/authZ, payment, data-migration, public-contract, or cross-repo).
+- **Sonnet** when: scoped but needs synthesis (implement a slice vs a frozen contract, wire several files, unit tests, ordinary debugging) **or** sub-decompose+verify Haiku leaves; acceptance criteria clear, path chooses among known patterns.
+- **Opus** when **ANY**: high ambiguity / design / cross-system judgment · high blast radius or irreversible · novel (no pattern) · the final completeness verdict.
+
+> **Blast-radius override (negative test):** a security/authZ, payment, migration, public-contract, or cross-repo node is **Opus even if it looks small/mechanical**. Blast radius beats specifiability — never route such a node to Haiku.
+
+## ESCALATE (the up-a-tier primitive)
+
+- **`ESCALATE: `** — "this task exceeds my tier." The parent **re-classifies** the node and re-runs it **one tier up**. A tier never guesses past its competence.
+- Distinct from **`BLOCKED: `** — that reaches a **human** for a decision/credential/input. Inside an async run, never stall: push what you have, then `ESCALATE:` (machine, up a tier) or `BLOCKED:` (human).
+
+## Reference Workflow scaffold (the deterministic encoder)
+
+The Workflow tool encodes the cascade so it runs the same way every time. Sketch:
+
+```js
+// Phase 1: Opus decomposes large-scoped work into a typed task tree (per-node tier).
+const tree = await agent({
+ model: "opus",
+ effort: "high",
+ prompt: `Decompose this scope into a task tree. For each leaf emit the hand-down spec
+ (objective, inputs, constraints, acceptance criteria, machine-check) and a tier
+ in {haiku, sonnet, opus} per the model-cascade rubric. SCOPE:\n${scope}`,
+ schema: TaskTreeSchema, // { nodes: [{ id, parent, spec, tier }] }
+});
+
+const TIERS = ["haiku", "sonnet", "opus"];
+const upOne = (t) => TIERS[Math.min(TIERS.indexOf(t) + 1, TIERS.length - 1)];
+
+// Phase 2: run each leaf at node.tier; ESCALATE re-runs it one tier up (re-classified).
+async function runNode(node) {
+ let tier = node.tier;
+ for (let attempt = 0; attempt < 3; attempt++) {
+ const out = await agent({ model: tier, prompt: renderSpec(node.spec) });
+ if (!out.text.startsWith("ESCALATE:")) return { node, tier, out };
+ tier = upOne(tier); // climb the ladder; never guess past competence
+ }
+ return { node, tier, escalatedToHuman: true }; // exhausted → BLOCKED to a human
+}
+const results = await pipeline(leaves(tree), runNode);
+
+// Phase 3: assemble bottom-up — each parent verifies its children vs the handed-down spec.
+// (a child never self-certifies; a lower tier never grades a higher tier.)
+const assembled = await assembleAndParentVerify(tree, results);
+
+// Phase 4: fresh Opus completeness verdict vs the ORIGINAL scope (separate from CI + QA).
+const verdict = await agent({
+ model: "opus",
+ effort: "high",
+ freshContext: true,
+ prompt: `Grade this assembled work against the ORIGINAL scope. Return PASS, GAPS(list),
+ or FAIL with reasons. SCOPE:\n${scope}\nWORK:\n${summarize(assembled)}`,
+ schema: VerdictSchema, // { verdict: "PASS"|"GAPS"|"FAIL", gaps?: string[] }
+});
+```
+
+## Commit-trailer rule (auditability)
+
+Fill the standing trailer with the **actual** tier that produced the commit, so git history shows which tier did what:
+
+```
+Co-Authored-By: Claude
+Claude-Session-Id:
+```
+
+e.g. `Co-Authored-By: Claude Sonnet 4.6 ` for a Sonnet-produced commit, `Claude Haiku 4.5` for a delegated mechanical leaf, `Claude Opus 4.8` for a decomposition/verdict commit.
+
+## Done
+
+Report per the `verification-protocol` skill: `SCOPE DONE (verified): …` + `OUT OF SCOPE — NOT DONE: …`. A node's verdict belongs to its parent; the *feature* verdict belongs to the orchestrator's fresh Opus pass — never to a single tier.
diff --git a/.claude/skills/repo-hardening/SKILL.md b/.claude/skills/repo-hardening/SKILL.md
new file mode 100644
index 0000000..b6967e7
--- /dev/null
+++ b/.claude/skills/repo-hardening/SKILL.md
@@ -0,0 +1,18 @@
+---
+name: repo-hardening
+description: Use to apply (or audit) the org branch-protection + Harden Gate + signing + community-health standard on a repo. Wraps governance/hardening-convention.md and scripts/. Owned operationally by devops-engineer under platform-governance policy.
+---
+
+# repo-hardening
+
+Applies the standard in `governance/hardening-convention.md`. Anti-lockout order matters.
+
+## Procedure
+1. **Land files via PR** — drop `workflow-templates/harden-gate.yml` + the automation stack + `community-templates/*` (incl. `CODEOWNERS`) onto a branch (`scripts/push_harden.sh`, `scripts/push_stack.sh`), open a PR.
+2. **Confirm the gate is green** on that PR (all six `gate-*` report success) before requiring anything.
+3. **Apply the ruleset** — `scripts/apply_ruleset.sh [extra,contexts]` builds `governance/ruleset.json` targeting `~DEFAULT_BRANCH`; require only confirmed-reporting contexts.
+4. **Signing-safe automation** — before enabling `required_signatures`, ensure any workflow that pushes directly to the default branch commits via the GitHub API (auto-signed) or runs as an admin/app bypass identity.
+5. **Deploy-on-push repos** (path-filtered deploys) — never bot-merge; merge in a deploy window.
+
+## Verify
+`gh api repos//rulesets` shows one active `Protect default branch` on `~DEFAULT_BRANCH` with the expected rules, contexts, signatures, and bypass actors (admin 5 + app 1236702). On a User account, GitHub Actions (15368) cannot be a bypass actor.
diff --git a/.claude/skills/service-cli/SKILL.md b/.claude/skills/service-cli/SKILL.md
new file mode 100644
index 0000000..42d7a53
--- /dev/null
+++ b/.claude/skills/service-cli/SKILL.md
@@ -0,0 +1,10 @@
+---
+name: service-cli
+description: Use when building/maintaining a microservice CLI. Conventions for command design, output contracts, and CI safety.
+---
+
+# service-cli
+
+Conventions: noun-verb command tree; `--json` machine output alongside human output; non-zero exit on error; no interactive prompts when stdin/stdout is piped; generate commands/types from the service contract; shell completion. Owned by cli-engineer.
+
+*(Structured stub — flesh out with concrete commands/checks as the first repo adopts it.)*
diff --git a/.claude/skills/ticket-creator/SKILL.md b/.claude/skills/ticket-creator/SKILL.md
new file mode 100644
index 0000000..513839b
--- /dev/null
+++ b/.claude/skills/ticket-creator/SKILL.md
@@ -0,0 +1,41 @@
+---
+name: ticket-creator
+description: Generate a complete, Jira-ready ticket (epic, story, or UX task) from a brief description; routes to the right ticket-type template. Use when creating Jira/FuzePlan tickets.
+skill: ticket-creator
+version: 1.0
+project: FuzePlan / PhoneDo
+---
+
+# Ticket Creator (Router)
+
+## Purpose
+Generates a complete, Jira-ready ticket from a brief description.
+Routes to the correct sub-skill based on ticket type.
+
+## Routing Table
+| Ticket Type | Sub-skill to load |
+|-------------|------------------|
+| Epic | `ticket-creator/epic/SKILL.md` |
+| Story | `ticket-creator/story/SKILL.md` |
+| UX Task | `ticket-creator/ux/SKILL.md` |
+| Frontend Task / Frontend Development | `ticket-creator/frontend/SKILL.md` |
+| Backend Task / Backend Development | `ticket-creator/backend/SKILL.md` |
+| QA Task (any test type) | `ticket-creator/qa/SKILL.md` (QA router) |
+| Documentation | `ticket-creator/docs/SKILL.md` |
+| DevOps Task | `ticket-creator/devops/SKILL.md` |
+| Bug | `ticket-creator/bug/SKILL.md` |
+
+## Universal Rules (apply to ALL types)
+1. Load sizing rules from `../shared/templates/SIZING.md` before generating any ticket.
+2. Load bug rules from `../shared/templates/BUG_RULES.md` before generating any Bug ticket.
+3. Replace ALL `[placeholder]` text with real content. Mark truly unknown fields as `[TBD — ask: ]`.
+4. Sub-task story points must be in `{2, 4, 8}` only. If the user estimates differently, correct it and explain why.
+5. Story must fit within 1 sprint. If the described scope won't fit, warn the user and suggest splitting.
+6. Output only the completed ticket — no preamble, no "here is your ticket", no markdown fence around the whole output.
+
+## If ticket type is unclear
+Ask: "What type of ticket do you want to create? (Epic / Story / UX Task / Frontend / Backend / QA / Docs / DevOps / Bug)"
+
+## If critical information is missing
+Ask targeted questions before generating. Never invent facts.
+Maximum 2–3 clarifying questions per generation.
diff --git a/.claude/skills/ticket-creator/backend/SKILL.md b/.claude/skills/ticket-creator/backend/SKILL.md
new file mode 100644
index 0000000..0763216
--- /dev/null
+++ b/.claude/skills/ticket-creator/backend/SKILL.md
@@ -0,0 +1,49 @@
+---
+skill: ticket-creator/backend
+---
+
+# Backend Task Creator
+
+## Purpose
+Creates a complete, Jira-ready Backend Development sub-ticket including full API spec.
+
+## Story Points reminder
+Valid values: {2, 4, 8}. 1 point = 1 hour.
+Estimate: 2=simple CRUD endpoint, 4=endpoint with business logic + validation, 8=complex service with DB changes.
+
+## Required information — ask if missing
+1. **What does this endpoint or service do?**
+2. **What HTTP method and path?** (optional — infer from description if possible)
+3. **Are there DB changes?** (new table / migration / index)
+4. **Which Story does this belong to?**
+
+## Template
+Load `../../shared/templates/backend.md` and fill it.
+
+## Field guidance
+
+### Title
+Format: `[Service name] — [Action] — [Entity]`
+Examples: "BillingService — Create invoice on renewal", "PartnerService — Add self-managed payment flag"
+
+### API Specification
+- Fill Method + Path even if approximate (the dev will correct during implementation)
+- Include ALL error responses that the business logic can produce — not just standard HTTP ones
+- Request body: include every field with its type and whether it is required
+- Response: include the exact shape with field names and types
+
+### Database Changes
+- If new table: include the full CREATE TABLE SQL with reasonable column types
+- If migration: describe precisely which column is being added, removed, or renamed
+- Mark as "No DB changes" explicitly if that is the case
+
+### Business Rule
+- Extract the core business rule from the description and state it explicitly in Acceptance Criteria
+- Example: "A partner with `isPaymentManagedByPartner=true` must NOT be charged via the platform gateway"
+
+### Testing Requirements
+- Minimum 4 unit test cases — always
+- Include at least: happy path, validation error, not-found, and one business rule enforcement test
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/bug/SKILL.md b/.claude/skills/ticket-creator/bug/SKILL.md
new file mode 100644
index 0000000..3344c41
--- /dev/null
+++ b/.claude/skills/ticket-creator/bug/SKILL.md
@@ -0,0 +1,51 @@
+---
+skill: ticket-creator/bug
+---
+
+# Bug Creator
+
+## Purpose
+Creates a complete, Jira-ready Bug ticket including severity assessment and optional spawn sub-tasks.
+
+## Before creating
+1. Load `../../shared/templates/BUG_RULES.md` to determine correct placement (under Epic or Story)
+2. Load `../../shared/templates/SIZING.md` to apply severity SLAs
+
+## Required information — ask if missing
+1. **What is broken? What is the impact?** (one sentence)
+2. **Steps to reproduce** (numbered list, at minimum 2 steps)
+3. **Expected vs actual behavior**
+4. **Severity** (Critical / High / Medium / Low)
+5. **Where was it found?** (Production / Staging / Dev)
+
+## Template
+Load `../../shared/templates/bug.md` and fill it.
+
+## Field guidance
+
+### Title
+Format: `[What's broken] — [impact]`
+Examples: "Auto-recharge charges partner balance instead of platform gateway — overcharging businesses"
+NOT: "Bug in billing" or "Fix billing issue"
+
+### Steps to Reproduce
+- Must be numbered, specific, and reproducible by someone who wasn't there
+- Include the exact URL, the exact button clicked, the exact input value
+- Minimum 3 steps
+
+### Severity vs Priority
+- **Severity** = impact on the system (data loss / core broken / degraded / cosmetic)
+- **Priority** = urgency to fix (aligned with SLAs from SIZING.md)
+- They can differ: a cosmetic bug on the payment confirmation page might have Low severity but High priority
+
+### Spawn Sub-Tasks
+- If the user confirms the fix requires > 2 hours of work, include the Spawn Sub-Tasks table
+- Mark Backend + QA (Unit + Functional) as required; others as optional
+- Add story points from {2, 4, 8} to each sub-task
+
+### Placement
+- If the bug was found during development of a specific story → parent = that Story, add "Relates to [STORY-ID]" link
+- If found independently in production → parent = the relevant Epic
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/devops/SKILL.md b/.claude/skills/ticket-creator/devops/SKILL.md
new file mode 100644
index 0000000..bfbee22
--- /dev/null
+++ b/.claude/skills/ticket-creator/devops/SKILL.md
@@ -0,0 +1,35 @@
+---
+skill: ticket-creator/devops
+---
+
+# DevOps Task Creator
+
+## Purpose
+Creates a complete, Jira-ready DevOps Task sub-ticket including rollback plan.
+
+## Required information — ask if missing
+1. **What infrastructure or configuration change is needed?**
+2. **Which environment?** (Dev / Staging / Production)
+3. **What is the risk level?** (Low / Medium / High)
+4. **Which Story does this belong to?**
+
+## Template
+Load `../../shared/templates/devops.md` and fill it.
+
+## Field guidance
+
+### Implementation Tasks
+- Write each step as a runbook-style command or action — specific enough to execute
+- Not: "Deploy the service" — but: "Run `helm upgrade billing-service ./charts/billing --set image.tag=v2.4.1`"
+
+### Rollback Plan
+- The trigger condition must be a measurable threshold (not "if something goes wrong")
+- The steps must be executable without the author being present
+
+### Risk Level
+- Low: config change, no downtime expected
+- Medium: rolling restart, brief degradation possible
+- High: data migration, schema change, or full service restart
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/docs/SKILL.md b/.claude/skills/ticket-creator/docs/SKILL.md
new file mode 100644
index 0000000..ae031cf
--- /dev/null
+++ b/.claude/skills/ticket-creator/docs/SKILL.md
@@ -0,0 +1,29 @@
+---
+skill: ticket-creator/docs
+---
+
+# Documentation Task Creator
+
+## Required information — ask if missing
+1. **What needs to be documented?**
+2. **Who is the audience?** (Developers / End Users / Partners / Ops)
+3. **Which Story does this belong to?**
+
+## Template
+Load `../../shared/templates/docs.md` and fill it.
+
+## Field guidance
+
+### Doc Type
+Choose the most specific type:
+- **API Reference** — endpoint docs for developers
+- **Architecture ADR** — architecture decision record
+- **User Guide** — step-by-step for end users or partners
+- **Runbook** — operational procedure for Ops/DevOps
+- **Research Note** — internal investigation or findings
+
+### What to Document
+- Must explain: who will read this, what they will do with it, and what is currently missing or confusing.
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/epic/SKILL.md b/.claude/skills/ticket-creator/epic/SKILL.md
new file mode 100644
index 0000000..68e8d5a
--- /dev/null
+++ b/.claude/skills/ticket-creator/epic/SKILL.md
@@ -0,0 +1,57 @@
+---
+skill: ticket-creator/epic
+---
+
+# Epic Creator
+
+## Purpose
+Creates a complete, Jira-ready Epic ticket from a brief description.
+
+## Sizing check (before creating)
+Load `../../shared/templates/SIZING.md`.
+Max Epic duration = N×D = 84 calendar days = 60 work days.
+If the described scope clearly exceeds 6 sprints, add a ⚠️ prefix to the title and include a note
+in the Problem Statement suggesting the Epic be decomposed.
+
+## Required information — ask if missing
+1. **What is the feature or initiative?** (1-sentence description)
+2. **Which domain?** (Billing / Auth / Notifications / Partner Portal / etc.)
+3. **What user or business problem does it solve?**
+
+## Template
+Load the template from `../../shared/templates/epic.md` and fill it.
+
+## Field guidance
+
+### Title
+Format: `[Verb] + [specific outcome]` — not "Improve billing", but "Enable partners to self-manage billing cycles"
+
+### Problem Statement
+- Must be 1–3 sentences
+- Must answer: "What breaks for the user/business today without this?"
+- Must explain: "Why is this important now?"
+
+### Goal
+- One sentence only, written as an observable outcome
+- Must be verifiable — "Partners can configure billing without contacting support" ✓ / "Better billing experience" ✗
+
+### Features In Scope
+- 3–6 items maximum
+- Each item = a specific, deliverable capability (not a vague theme)
+- If the user gives > 6 items, suggest splitting into 2 Epics and flag with ⚠️
+
+### Success Metrics
+- Must be quantifiable
+- Include a current baseline if known; write `[Establish baseline in Sprint 1]` if unknown
+- At least 1 row required
+
+### Child Stories
+- Leave empty (will be populated during Story Planning phase)
+
+### Dependencies
+- Only fill if the user explicitly mentions dependencies
+- Leave as `—` if none known
+
+## Output rules
+- Output the filled template only, no preamble
+- Use Jira-compatible markdown (bold, tables, checklists with `- [ ]`)
diff --git a/.claude/skills/ticket-creator/frontend/SKILL.md b/.claude/skills/ticket-creator/frontend/SKILL.md
new file mode 100644
index 0000000..6a5f03e
--- /dev/null
+++ b/.claude/skills/ticket-creator/frontend/SKILL.md
@@ -0,0 +1,47 @@
+---
+skill: ticket-creator/frontend
+---
+
+# Frontend Task Creator
+
+## Purpose
+Creates a complete, Jira-ready Frontend Development sub-ticket.
+
+## Story Points reminder
+Valid values: {2, 4, 8} only. 1 point = 1 hour.
+Estimate based on: 2=simple isolated component, 4=component with API + state, 8=full page or complex flow.
+
+## Required information — ask if missing
+1. **What UI component or page is being built?**
+2. **What API endpoint does it consume?** (optional — fill with [TBD] if unknown)
+3. **Is there a Figma link?** (optional — note if missing)
+4. **Which Story does this belong to?**
+
+## Template
+Load `../../shared/templates/frontend.md` and fill it.
+
+## Field guidance
+
+### Title
+Format: `[Component/Page name] — [Action]`
+Examples: "PaymentMethodCard — Build with card-on-file display", "Partner Invoice List — Implement with pagination"
+
+### Implementation Tasks
+- List 5–8 concrete tasks
+- Each task = one atomic implementation step (not a feature description)
+- Include API connection, state handling, validation, responsiveness explicitly
+
+### Component Props Interface
+- Write TypeScript interface if the user provides component details
+- If unknown, write `[TBD — developer to define during implementation]`
+
+### Acceptance Criteria
+- Must include: Figma match, all 4 states (loading/error/empty/populated), validation, a11y score ≥ 90
+- Add domain-specific ACs from the user's description
+
+### Testing Requirements
+- Minimum 4 unit test cases — always
+- Tests must be specific (not "test that it renders")
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/qa/SKILL.md b/.claude/skills/ticket-creator/qa/SKILL.md
new file mode 100644
index 0000000..fe0c832
--- /dev/null
+++ b/.claude/skills/ticket-creator/qa/SKILL.md
@@ -0,0 +1,26 @@
+---
+skill: ticket-creator/qa
+version: 1.0
+---
+
+# QA Task Creator (Router)
+
+## Purpose
+Routes to the correct QA sub-type skill based on the type of test being created.
+
+## Routing Table
+| Test Type | Sub-skill |
+|-----------|-----------|
+| Unit | `unit/SKILL.md` |
+| Functional / E2E | `functional/SKILL.md` |
+| Integration | `integration/SKILL.md` |
+| Load Test / Stress Test / Performance | `load-stress/SKILL.md` |
+| Security / Penetration / OWASP | `security/SKILL.md` |
+
+## If test type is unclear
+Ask: "What type of QA task? (Unit / Functional / Integration / Load-Stress / Security)"
+
+## Universal QA rules
+- Story Points must be in {2, 4, 8}. Unit tests: 2–4 pts. Functional: 4–8 pts. Load/Security: 4–8 pts.
+- Assignee for Unit tests = the developer who built the feature (not the QA engineer)
+- Environment: Unit = any; Functional/Integration = Staging; Load/Security = Staging only, NEVER Production
diff --git a/.claude/skills/ticket-creator/qa/functional/SKILL.md b/.claude/skills/ticket-creator/qa/functional/SKILL.md
new file mode 100644
index 0000000..4399993
--- /dev/null
+++ b/.claude/skills/ticket-creator/qa/functional/SKILL.md
@@ -0,0 +1,25 @@
+---
+skill: ticket-creator/qa/functional
+---
+
+# Functional Test Creator
+
+## Required information — ask if missing
+1. **What user flow or feature is being tested end-to-end?**
+2. **Which Story does this belong to?**
+
+## Template
+Load `../../../shared/templates/qa-functional.md` and fill it.
+
+## Field guidance
+
+### Test Scenarios
+- Minimum: 2 happy path, 1 edge case, 4 negative/error scenarios — always
+- Each scenario must have explicit steps, not just a description
+
+### Test Data
+- Name the exact test accounts needed
+- Describe any seed data that must exist in staging before the test runs
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/qa/integration/SKILL.md b/.claude/skills/ticket-creator/qa/integration/SKILL.md
new file mode 100644
index 0000000..661036d
--- /dev/null
+++ b/.claude/skills/ticket-creator/qa/integration/SKILL.md
@@ -0,0 +1,25 @@
+---
+skill: ticket-creator/qa/integration
+---
+
+# Integration Test Creator
+
+## Required information — ask if missing
+1. **Which services or systems are being integrated?** (e.g., BillingService ↔ Cardcom gateway)
+2. **Which Story does this belong to?**
+
+## Template
+Load `../../../shared/templates/qa-integration.md` and fill it.
+
+## Field guidance
+
+### Integration Points Table
+- List every pair of systems that communicate
+- Note the protocol (REST / DB / Queue / Webhook)
+
+### Mock vs Real
+- State clearly which dependencies are real and which are mocked
+- Explain why (cost, availability, isolation)
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/qa/load-stress/SKILL.md b/.claude/skills/ticket-creator/qa/load-stress/SKILL.md
new file mode 100644
index 0000000..1239ac3
--- /dev/null
+++ b/.claude/skills/ticket-creator/qa/load-stress/SKILL.md
@@ -0,0 +1,25 @@
+---
+skill: ticket-creator/qa/load-stress
+---
+
+# Load & Stress Test Creator
+
+## Required information — ask if missing
+1. **Which endpoints or flows are being load-tested?**
+2. **What business scenario does the test simulate?** (e.g., "End-of-month batch billing for 5,000 businesses")
+3. **Which Story does this belong to?**
+
+## Template
+Load `../../../shared/templates/qa-load-stress.md` and fill it.
+
+## Field guidance
+
+### Performance Targets
+- Fill all 6 rows of the targets table
+- If the user doesn't provide targets, use the template defaults and mark them as `[TBD — confirm with DevOps]`
+
+### Scenarios
+- All 4 scenarios (Baseline, Peak, Stress, Spike) are required — always
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/qa/security/SKILL.md b/.claude/skills/ticket-creator/qa/security/SKILL.md
new file mode 100644
index 0000000..fdf9248
--- /dev/null
+++ b/.claude/skills/ticket-creator/qa/security/SKILL.md
@@ -0,0 +1,25 @@
+---
+skill: ticket-creator/qa/security
+---
+
+# Security Test Creator
+
+## Required information — ask if missing
+1. **What feature or set of endpoints is in scope?**
+2. **Which Story does this belong to?**
+
+## Template
+Load `../../../shared/templates/qa-security.md` and fill it.
+
+## Field guidance
+
+### OWASP Checklist
+- Keep all 10 rows — never remove any
+- Customize the "Test Method" column for the specific feature being tested
+
+### Feature-Specific Test Cases
+- The 6 rows in the template are the minimum — always include all 6
+- Add feature-specific rows for any unique auth patterns or file upload flows
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/qa/unit/SKILL.md b/.claude/skills/ticket-creator/qa/unit/SKILL.md
new file mode 100644
index 0000000..4b0a241
--- /dev/null
+++ b/.claude/skills/ticket-creator/qa/unit/SKILL.md
@@ -0,0 +1,25 @@
+---
+skill: ticket-creator/qa/unit
+---
+
+# Unit Test Creator
+
+## Required information — ask if missing
+1. **What module, service, or component is being tested?**
+2. **Which Story does this belong to?**
+
+## Template
+Load `../../../shared/templates/qa-unit.md` and fill it.
+
+## Field guidance
+
+### Test Scenarios
+- Must include: at least 2 happy path, 2 edge cases, 1 error case — minimum
+- Be specific: "Given balance = 0 When auto-recharge triggers Then partner gateway is charged" ✓
+
+### Coverage Requirements
+- Always require ≥ 80% overall
+- List the specific functions that must hit 100% coverage
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/story/SKILL.md b/.claude/skills/ticket-creator/story/SKILL.md
new file mode 100644
index 0000000..4dfe257
--- /dev/null
+++ b/.claude/skills/ticket-creator/story/SKILL.md
@@ -0,0 +1,56 @@
+---
+skill: ticket-creator/story
+---
+
+# Story Creator
+
+## Purpose
+Creates a complete, Jira-ready Story ticket from a brief description.
+
+## Sizing check (before creating)
+Load `../../shared/templates/SIZING.md`.
+A Story must fit within 1 sprint (W=10 work days). Check the critical path of described sub-tasks.
+If scope is clearly > 1 sprint, warn: "This story may need to be split. I'll generate it, but flag sub-tasks
+that should become a separate story."
+
+## Required information — ask if missing
+1. **What does the user want to achieve?** (1-sentence user need)
+2. **Who is the user?** (persona: admin / partner / business / agent)
+3. **Which Epic does this belong to?** (Epic ID or description)
+
+## Template
+Load the template from `../../shared/templates/story.md` and fill it.
+
+## Field guidance
+
+### Title
+Format: User-goal sentence — "Partner can view consolidated invoice for all managed businesses"
+NOT task-format — "Create invoice endpoint" belongs in a sub-task, not the story title.
+
+### User Story (As a / I want / So that)
+- "As a" = specific persona (not "As a user")
+- "I want to" = the action, not the implementation
+- "So that" = the business or user benefit
+
+### Acceptance Criteria
+- Minimum 2 Given/When/Then items — always
+- Must include at least 1 edge case and 1 error case
+- Format strictly: "**Given** X **When** Y **Then** Z"
+- No vague criteria: "Then the system works correctly" is not acceptable
+
+### Story Points
+- = sum of all sub-task story points
+- Each sub-task must be 2, 4, or 8 points
+- Calculate after listing sub-tasks
+
+### Sub-Tasks
+- List every implementation task as a typed sub-task (UX / Backend / Frontend / QA / Docs)
+- Assign realistic story points from {2, 4, 8} to each
+- At minimum include: at least 1 dev task (BE or FE) + at least 1 QA task
+
+### Definition of Done
+- Include all 7 standard DoD items from the template
+- Add domain-specific items if the user mentions them (e.g., "needs partner notification")
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-creator/ux/SKILL.md b/.claude/skills/ticket-creator/ux/SKILL.md
new file mode 100644
index 0000000..5e9eada
--- /dev/null
+++ b/.claude/skills/ticket-creator/ux/SKILL.md
@@ -0,0 +1,38 @@
+---
+skill: ticket-creator/ux
+---
+
+# UX Task Creator
+
+## Purpose
+Creates a complete, Jira-ready UX Task sub-ticket from a brief design brief.
+
+## Required information — ask if missing
+1. **What screen or flow is being designed?**
+2. **What is the user trying to accomplish?**
+3. **Which Story does this belong to?** (Story ID)
+
+## Template
+Load `../../shared/templates/ux.md` and fill it.
+
+## Field guidance
+
+### Title
+Format: `[Screen name / Flow] — [Action]`
+Examples: "Partner Billing Overview — Design all states", "Auto-recharge Setup Flow — Redesign error handling"
+
+### Design Goals
+- 2–3 specific, measurable goals
+- "Improve UX" is not a goal — "Reduce checkout steps from 4 to 2" is
+
+### Required States
+- All 7 states in the template are required by default
+- If a specific state genuinely doesn't apply (e.g., no empty state for a settings form), strike it through with a reason
+
+### User Flow
+- Number each step
+- Each step = one atomic action by the user or one system response
+- Cover the happy path + the most critical error path
+
+## Output rules
+- Output the filled template only, no preamble
diff --git a/.claude/skills/ticket-enforcer/SKILL.md b/.claude/skills/ticket-enforcer/SKILL.md
new file mode 100644
index 0000000..79cf8a4
--- /dev/null
+++ b/.claude/skills/ticket-enforcer/SKILL.md
@@ -0,0 +1,187 @@
+---
+name: ticket-enforcer
+description: Audit a whole Jira project or sprint for ticket-quality compliance in bulk, producing per-ticket scores and a project-level health summary. Use to enforce Jira ticket quality across many tickets.
+skill: ticket-enforcer
+version: 1.0
+project: FuzePlan / PhoneDo
+---
+
+# Ticket Enforcer
+
+## Purpose
+Audits an entire project or sprint for ticket quality compliance.
+Applies the same completeness rules as the ticket-reviewer to every ticket in bulk.
+Produces an aggregate compliance report with per-ticket scores and a project-level health summary.
+
+## Input
+- A Jira project key (e.g., `BLNG`) — audits all open tickets
+- OR a sprint name + project key — audits all tickets in that sprint
+- OR a comma-separated list of ticket IDs — audits those specific tickets
+
+## Step 1 — Fetch tickets
+Use the Atlassian MCP tool to fetch tickets.
+
+**For a project audit:**
+```
+JQL: project = [PROJECT_KEY] AND status != Closed ORDER BY issuetype ASC
+Fields: summary, description, issuetype, status, assignee, priority, labels, parent, story_points, created, updated
+```
+
+**For a sprint audit:**
+```
+JQL: project = [PROJECT_KEY] AND sprint = "[SPRINT_NAME]"
+Fields: same as above
+```
+
+Fetch in batches of up to 50. Continue paginating until all tickets are retrieved.
+
+## Step 2 — Group by type
+Group tickets into these categories:
+- **Epic** — Epic type
+- **Story** — Story type
+- **Dev Task** — Frontend Development, Backend Development, UX Task, Sub-task
+- **QA Task** — Any QA or Testing type
+- **Documentation** — Documentation type
+- **DevOps Task** — DevOps type
+- **Bug** — Bug type
+- **Unknown** — Anything not in the above list (flag for type cleanup)
+
+## Step 3 — Apply rules per ticket
+
+### Universal rules (all types)
+| Check | Green ✅ | Yellow 🟡 | Red 🔴 |
+|-------|---------|---------|------|
+| Summary | Verb + specific outcome | Vague but present | Missing or just type name |
+| Assignee | Set | — | Unassigned (if not Backlog/Ideation) |
+| Priority | Set | — | None |
+| Description | > 100 chars | > 30 chars | Empty or < 30 chars |
+
+### Epic-specific rules
+| Check | Green ✅ | Red 🔴 |
+|-------|---------|------|
+| Problem Statement | Present and > 50 chars | Missing |
+| Goal | Present | Missing |
+| Features In Scope | ≥ 3 items | < 3 or missing |
+| Success Metrics | ≥ 1 metric with target | Missing |
+| Child Stories | ≥ 1 story linked (if past Ideation) | None linked + not in Ideation |
+| Sizing | Duration ≤ N×D = 84 days | > 84 days in progress |
+
+### Story-specific rules
+| Check | Green ✅ | Red 🔴 |
+|-------|---------|------|
+| Parent Epic | Linked | Not linked |
+| User Story format | All 3 parts present | Missing or incomplete |
+| Acceptance Criteria | ≥ 2 Given/When/Then | < 2 or no AC |
+| Definition of Done | ≥ 5 items | < 3 items |
+| Sub-Tasks | ≥ 1 dev + ≥ 1 QA | None |
+| Story Points | Set and valid {2,4,8} sums | Not set or invalid |
+| Sprint (if In Progress) | Assigned | Missing |
+
+### Dev Task-specific rules (Frontend / Backend / UX / Sub-task)
+| Check | Green ✅ | Red 🔴 |
+|-------|---------|------|
+| Parent Story | Linked | Not linked |
+| Story Points | In {2, 4, 8} | Not set or invalid |
+| Implementation Tasks | ≥ 4 items | < 2 items |
+| Acceptance Criteria | ≥ 3 items | Missing |
+| Testing Requirements | ≥ 2 named tests | Missing |
+| Backend: API Spec | Method + path + response present | Missing |
+| Frontend: Figma link | Present | Missing |
+
+### QA Task-specific rules
+| Check | Green ✅ | Red 🔴 |
+|-------|---------|------|
+| Test Type | Stated | Missing |
+| Story Points | In {2, 4, 8} | Not set |
+| Test Scenarios | ≥ 3 rows (Happy + Edge + Error) | Missing |
+| Environment | Stated | Missing |
+| Load/Stress: Perf targets | All 6 metrics present | Missing |
+| Security: OWASP checklist | All 10 rows present | Missing |
+
+### Documentation-specific rules
+| Check | Green ✅ | Red 🔴 |
+|-------|---------|------|
+| Doc Type | Stated | Missing |
+| Audience | Named | Missing |
+| Deliverables | ≥ 1 document with location | Missing |
+| Acceptance Criteria | ≥ 3 items | Missing |
+
+### DevOps Task-specific rules
+| Check | Green ✅ | Red 🔴 |
+|-------|---------|------|
+| Environment | Stated | Missing |
+| Rollback Plan | Trigger + steps | Missing entirely |
+| Security Checklist | All 4 items | Missing |
+| Implementation Tasks | ≥ 4 specific steps | < 2 or vague |
+
+### Bug-specific rules
+| Check | Green ✅ | Red 🔴 |
+|-------|---------|------|
+| Severity | Set | Not set |
+| Steps to Reproduce | ≥ 3 steps | < 2 or missing |
+| Expected + Actual | Both present > 20 chars | Either missing |
+| Evidence | ≥ 1 item attached | All unchecked |
+| Severity SLA | Assignee set within SLA window | Critical bug unassigned |
+
+## Step 4 — Score each ticket
+For each ticket:
+- 🟢 **Green**: 0 red flags + ≤ 1 yellow flag
+- 🟡 **Yellow**: 0 red flags + 2–3 yellow flags
+- 🔴 **Red**: ≥ 1 red flag OR ≥ 4 yellow flags
+
+## Step 5 — Generate compliance report
+
+Output the report in this format:
+
+```
+# Ticket Compliance Report
+**Project:** [PROJECT_KEY] **Sprint / Scope:** [SPRINT or "All open tickets"]
+**Run date:** [YYYY-MM-DD] **Total tickets audited:** [N]
+
+---
+
+## Summary
+| Score | Count | % |
+|-------|-------|---|
+| 🟢 Green | N | X% |
+| 🟡 Yellow | N | X% |
+| 🔴 Red | N | X% |
+
+**Project health:** 🟢 Healthy / 🟡 Needs attention / 🔴 Action required
+
+---
+
+## 🔴 Red Tickets (fix before next sprint)
+| Ticket | Type | Summary | Issues |
+|--------|------|---------|--------|
+| [ID] | [Type] | [Summary] | [Comma-separated list of red flags] |
+...
+
+## 🟡 Yellow Tickets (improve before moving to next status)
+| Ticket | Type | Summary | Suggestions |
+|--------|------|---------|-------------|
+| [ID] | [Type] | [Summary] | [Comma-separated list of yellow flags] |
+...
+
+## 🟢 Green Tickets
+[N] tickets are complete and well-formed. ✅
+
+---
+
+## Systemic Issues
+[If ≥ 30% of tickets of the same type share a common gap, call it out here as a pattern.]
+Example: "8 of 11 Stories are missing edge cases in Acceptance Criteria. Consider a story-writing session."
+
+---
+
+## Recommended Actions
+1. [Most impactful fix, assigned to a team role]
+2. [Second most impactful fix]
+3. [Third]
+```
+
+## Notes
+- This skill reads data; it does NOT modify any Jira tickets.
+- If a ticket type cannot be identified, include it in an "Unknown Type" section and suggest setting the correct issue type.
+- Apply bug placement rules from `../shared/templates/BUG_RULES.md` when reviewing Bug tickets.
+- Apply sizing rules from `../shared/templates/SIZING.md` for Epic duration and Story point validation.
diff --git a/.claude/skills/ticket-reviewer/SKILL.md b/.claude/skills/ticket-reviewer/SKILL.md
new file mode 100644
index 0000000..8132ae3
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/SKILL.md
@@ -0,0 +1,61 @@
+---
+name: ticket-reviewer
+description: Review a single existing Jira ticket for completeness and quality, returning a structured score with gap analysis and concrete improvement suggestions. Use to assess/improve one Jira ticket.
+skill: ticket-reviewer
+version: 1.0
+project: FuzePlan / PhoneDo
+---
+
+# Ticket Reviewer (Router)
+
+## Purpose
+Reviews an existing ticket for completeness and quality.
+Returns a structured score with gap analysis and concrete improvement suggestions.
+
+## Input
+Ticket content (from Jira via MCP fetch, or pasted text).
+If fetching from Jira, retrieve: summary, description, issuetype, status, assignee, priority, labels, parent, story points.
+
+## Routing Table
+| Ticket Type | Sub-skill |
+|-------------|-----------|
+| Epic | `epic/SKILL.md` |
+| Story | `story/SKILL.md` |
+| Frontend Development / Backend Development / Sub-task | `dev-task/SKILL.md` |
+| QA Task (any sub-type) | `qa/SKILL.md` |
+| Documentation | `docs/SKILL.md` |
+| DevOps Task | `devops/SKILL.md` |
+| Bug | `bug/SKILL.md` |
+| UX Task | `dev-task/SKILL.md` (uses same review structure as dev tasks) |
+
+## Universal Fields (check on ALL ticket types)
+| Field | Green ✅ | Yellow 🟡 | Red 🔴 |
+|-------|---------|---------|------|
+| Summary | Action verb + specific outcome | Vague but present | Missing or just ticket type name |
+| Assignee | Set | — | Unassigned + not in Ideation/Backlog |
+| Priority | Set | — | None / not set |
+| Description | Present and > 100 chars | Present but < 100 chars | Empty |
+
+## Output Format
+```
+## Review: [TICKET-ID] — [Ticket Type]
+
+**Overall: 🟢 Strong / 🟡 Needs work / 🔴 Incomplete**
+
+| Section | Score | Gap |
+|---------|-------|-----|
+| [Field] | 🟢/🟡/🔴 | [Issue or —] |
+...
+
+### ⚠️ Required before moving to next status
+1. [Specific fix required]
+2. [Specific fix required]
+
+### 💡 Suggested improvements
+- [Optional improvement]
+```
+
+## Scoring logic
+- 🟢 Green: 0 red + ≤ 1 yellow → **Strong**
+- 🟡 Yellow: 0 red + 2–3 yellows → **Needs work**
+- 🔴 Red: any red OR 4+ yellows → **Incomplete**
diff --git a/.claude/skills/ticket-reviewer/bug/SKILL.md b/.claude/skills/ticket-reviewer/bug/SKILL.md
new file mode 100644
index 0000000..89ae77c
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/bug/SKILL.md
@@ -0,0 +1,31 @@
+---
+skill: ticket-reviewer/bug
+---
+
+# Bug Reviewer
+
+## Review Checklist
+
+Apply universal fields from `../SKILL.md`, then:
+
+| Section | Green ✅ | Yellow 🟡 | Red 🔴 |
+|---------|---------|---------|------|
+| Severity | Set (Critical / High / Medium / Low) | — | Not set |
+| Summary | States what's broken + business impact in 1 sentence | Vague impact | Just "bug in X" |
+| Steps to Reproduce | ≥ 3 numbered steps, specific enough to reproduce | ≥ 2 steps but vague | < 2 steps or missing |
+| Expected vs Actual | Both present, each ≥ 20 chars and specific | One missing or vague | Both missing |
+| Environment Details | Browser/Client + User Role + URL present | 1–2 fields present | Missing |
+| Evidence | ≥ 1 item checked (screenshot / Sentry / logs) | — | All unchecked |
+| Parent (Epic or Story) | Linked to Epic or Story | — | Not linked |
+| Bug rules | Correct placement per BUG_RULES.md | — | Bug under wrong parent |
+
+## Severity SLA check
+Load `../../shared/templates/SIZING.md`.
+Check assignment SLA: if Severity=Critical and Assignee is empty or bug is in Opened status for > 4h, flag: "⚠️ Critical bug SLA breach risk."
+
+## Spawn sub-tasks check
+If the bug has been In Progress for a while but has no sub-tasks and the fix is non-trivial, suggest:
+"💡 This fix appears to require significant work. Consider spawning sub-tasks per BUG_RULES.md."
+
+## Output
+Use the format from `../SKILL.md`.
diff --git a/.claude/skills/ticket-reviewer/dev-task/SKILL.md b/.claude/skills/ticket-reviewer/dev-task/SKILL.md
new file mode 100644
index 0000000..67984f5
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/dev-task/SKILL.md
@@ -0,0 +1,51 @@
+---
+skill: ticket-reviewer/dev-task
+---
+
+# Dev Task Reviewer (Frontend · Backend · UX · DevOps)
+
+## Purpose
+Reviews Frontend Development, Backend Development, UX Task, and DevOps Task sub-tickets.
+These share the same review structure (implementation tasks + ACs + testing).
+
+## Review Checklist
+
+Apply universal fields from `../SKILL.md`, then:
+
+| Section | Green ✅ | Yellow 🟡 | Red 🔴 |
+|---------|---------|---------|------|
+| Parent Story | Linked to a Story | — | Not linked |
+| Story Points | In {2, 4, 8} | — | Not set or invalid value |
+| Context | ≥ 1 sentence explaining what + why | Present but < 30 chars | Missing |
+| Implementation Tasks | ≥ 4 specific, actionable items | 2–3 items or vague | < 2 items |
+| Acceptance Criteria | ≥ 3 specific, verifiable items | 1–2 items | Missing |
+| Testing Requirements | ≥ 2 specific test cases (named) | "write unit tests" (generic) | Missing |
+| References | Parent story + Figma (if FE/UX) | One ref missing | No refs |
+
+## Backend-specific checks
+| Section | Green ✅ | Red 🔴 |
+|---------|---------|------|
+| API Specification | Method, path, auth, request body, response, errors all present | Any of these missing |
+| DB Changes | Explicitly stated (even if "No DB changes") | Not mentioned |
+| Business rule | At least 1 explicit business rule in ACs | ACs are only technical (no business logic) |
+
+## Frontend-specific checks
+| Section | Green ✅ | Red 🔴 |
+|---------|---------|------|
+| Figma link | Present | Missing (for any FE task) |
+| All 4 states | Loading/Error/Empty/Populated in ACs | Any state missing |
+
+## UX-specific checks
+| Section | Green ✅ | Red 🔴 |
+|---------|---------|------|
+| Required states | All 7 states listed | < 5 states |
+| Deliverables | Wireframes + HiFi + handoff noted | Only "design the screen" |
+
+## DevOps-specific checks
+| Section | Green ✅ | Red 🔴 |
+|---------|---------|------|
+| Rollback Plan | Trigger + steps present | Missing |
+| Security checklist | All 4 items present | Missing |
+
+## Output
+Use the format from `../SKILL.md`.
diff --git a/.claude/skills/ticket-reviewer/devops/SKILL.md b/.claude/skills/ticket-reviewer/devops/SKILL.md
new file mode 100644
index 0000000..bba8f03
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/devops/SKILL.md
@@ -0,0 +1,24 @@
+---
+skill: ticket-reviewer/devops
+---
+
+# DevOps Task Reviewer
+
+## Review Checklist
+
+Apply universal fields from `../SKILL.md`, then:
+
+| Section | Green ✅ | Yellow 🟡 | Red 🔴 |
+|---------|---------|---------|------|
+| Environment | Explicitly stated (Dev / Staging / Production) | — | Missing |
+| Risk Level | Stated (Low / Medium / High) | — | Missing |
+| Planned Window | Date and time stated | — | Missing for Production changes |
+| Implementation Tasks | ≥ 4 runbook-style steps (specific commands or actions) | 2–3 steps | < 2 or just "deploy the service" |
+| Configuration Changes | Before/after config shown | Mentioned but no detail | Missing if change exists |
+| Pre-deployment Checklist | ≥ 4 of 5 items checked | 2–3 items | Missing |
+| Rollback Plan | Trigger + ≥ 2 steps present | Steps without trigger | Missing entirely |
+| Security Checklist | All 4 items present | 2–3 items | Missing |
+| Acceptance Criteria | Health check + monitoring criteria stated | Only "it works" | Missing |
+
+## Output
+Use the format from `../SKILL.md`.
diff --git a/.claude/skills/ticket-reviewer/docs/SKILL.md b/.claude/skills/ticket-reviewer/docs/SKILL.md
new file mode 100644
index 0000000..5901e08
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/docs/SKILL.md
@@ -0,0 +1,22 @@
+---
+skill: ticket-reviewer/docs
+---
+
+# Documentation Task Reviewer
+
+## Review Checklist
+
+Apply universal fields from `../SKILL.md`, then:
+
+| Section | Green ✅ | Yellow 🟡 | Red 🔴 |
+|---------|---------|---------|------|
+| Doc Type | Specific type stated | — | Missing |
+| Audience | Named (Developers / End Users / Partners / Ops) | — | Missing |
+| Published Location | URL or path stated | — | Missing |
+| What to Document | Specific topic + audience + gap explained | Vague description | Missing |
+| Deliverables | ≥ 1 named document with location | "write docs" (generic) | Missing |
+| Required Sections | ≥ 5 of 8 standard sections listed | 3–4 sections | < 3 sections |
+| Acceptance Criteria | ≥ 3 items including "code examples tested" and "reviewed by X" | 1–2 items | Missing |
+
+## Output
+Use the format from `../SKILL.md`.
diff --git a/.claude/skills/ticket-reviewer/epic/SKILL.md b/.claude/skills/ticket-reviewer/epic/SKILL.md
new file mode 100644
index 0000000..47cdb77
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/epic/SKILL.md
@@ -0,0 +1,27 @@
+---
+skill: ticket-reviewer/epic
+---
+
+# Epic Reviewer
+
+## Review Checklist
+
+Apply universal fields from `../SKILL.md`, then the following:
+
+| Section | Green ✅ | Yellow 🟡 | Red 🔴 |
+|---------|---------|---------|------|
+| Problem Statement | 1–3 sentences, names the pain + explains "why now" | Present but vague (< 50 chars or restates title) | Missing |
+| Goal | One sentence, observable outcome | Present but unmeasurable ("improve experience") | Missing |
+| Target Personas | ≥ 2 named personas with role context | ≥ 1 named persona | Missing |
+| Features In Scope | ≥ 3 specific items (not vague themes) | 1–2 items or vague items | Missing |
+| Out of Scope | ≥ 1 explicit exclusion with reason | Present but empty or generic | Missing |
+| Success Metrics | ≥ 1 row with measurable target | Present but no target value | Missing |
+| Child Stories | ≥ 1 story listed (if Epic is past Ideation) | Empty but Epic is in Ideation | Empty + Epic is In Progress |
+| References | ≥ 1 design or spec link | — | Missing if Epic is past Planning |
+
+## Sizing check
+Load `../../shared/templates/SIZING.md`.
+If Epic has been In Progress for > N×D days, flag: "⚠️ Epic may be over-budget (N=6, D=14). Consider decomposing."
+
+## Output
+Use the format from `../SKILL.md`.
diff --git a/.claude/skills/ticket-reviewer/qa/SKILL.md b/.claude/skills/ticket-reviewer/qa/SKILL.md
new file mode 100644
index 0000000..ab9dade
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/qa/SKILL.md
@@ -0,0 +1,40 @@
+---
+skill: ticket-reviewer/qa
+---
+
+# QA Task Reviewer (all sub-types)
+
+## Purpose
+Reviews QA Tasks of any sub-type: Unit, Functional, Integration, Load/Stress, Security.
+The review structure is the same — only the type-specific checks differ.
+
+## Review Checklist
+
+Apply universal fields from `../SKILL.md`, then:
+
+| Section | Green ✅ | Yellow 🟡 | Red 🔴 |
+|---------|---------|---------|------|
+| Test Type | Explicitly stated (Unit/Functional/Integration/Load-Stress/Security) | — | Missing |
+| Parent Story | Linked | — | Not linked |
+| Story Points | In {2, 4, 8} | — | Not set |
+| What to Test | 1–2 sentences, specific | Vague but present | Missing |
+| Test Scenarios | ≥ 1 table with ≥ 3 rows (Happy + Edge + Error) | ≥ 1 row present | No scenarios |
+| Acceptance Criteria | ≥ 2 specific, verifiable items | 1 item | Missing |
+| Environment | Explicitly stated | — | Missing |
+| Tool | Named (Jest / Cypress / k6 / ZAP / etc.) | — | Missing |
+
+## Unit-specific
+- Assignee must be the developer (not a QA engineer) → flag if it appears to be a QA engineer
+- Coverage requirement must be stated
+
+## Load/Stress-specific
+- Performance targets table must be present with ≥ 4 metrics
+- All 4 scenarios (Baseline / Peak / Stress / Spike) must be present
+- Environment note "Staging only — NOT Production" must be explicit
+
+## Security-specific
+- OWASP checklist must be present (all 10 rows)
+- Feature-specific test cases must include at minimum: auth bypass + IDOR
+
+## Output
+Use the format from `../SKILL.md`.
diff --git a/.claude/skills/ticket-reviewer/story/SKILL.md b/.claude/skills/ticket-reviewer/story/SKILL.md
new file mode 100644
index 0000000..cffe10d
--- /dev/null
+++ b/.claude/skills/ticket-reviewer/story/SKILL.md
@@ -0,0 +1,28 @@
+---
+skill: ticket-reviewer/story
+---
+
+# Story Reviewer
+
+## Review Checklist
+
+Apply universal fields from `../SKILL.md`, then:
+
+| Section | Green ✅ | Yellow 🟡 | Red 🔴 |
+|---------|---------|---------|------|
+| Parent Epic | Linked to an Epic | — | Not linked |
+| User Story format | As a [persona] / I want / So that — all 3 parts present | Present but vague persona ("user" / "admin") | Missing or not in this format |
+| Acceptance Criteria | ≥ 2 Given/When/Then items + ≥ 1 edge case + ≥ 1 error case | ≥ 2 items but no edge/error case | < 2 items or no AC at all |
+| Definition of Done | ≥ 5 of 7 standard items present | 3–4 items | < 3 items |
+| Sub-Tasks | ≥ 1 dev task (BE or FE) + ≥ 1 QA task | ≥ 1 task total | No sub-tasks |
+| Story Points | Set, equals sum of sub-task points from {2,4,8} | Set but doesn't match sub-task sum | Not set |
+| Sprint | Assigned (if status ≠ Backlog) | — | In Progress but no sprint |
+| References | Figma link present (if FE work involved) | — | No refs if design exists |
+
+## Sizing check
+Load `../../shared/templates/SIZING.md`.
+Check: if critical path of sub-tasks (longest sequential chain per developer) > W=10 days, flag:
+"⚠️ Story may exceed 1 sprint. Suggest splitting at: [point where it could be split]."
+
+## Output
+Use the format from `../SKILL.md`.
diff --git a/.claude/skills/ui-frame-contract/SKILL.md b/.claude/skills/ui-frame-contract/SKILL.md
new file mode 100644
index 0000000..791783e
--- /dev/null
+++ b/.claude/skills/ui-frame-contract/SKILL.md
@@ -0,0 +1,48 @@
+---
+name: ui-frame-contract
+description: Use in the frontend-design phase, before feature-UI implementation. Produce static HTML frame(s) — a single page or a SEQUENCE demonstrating the flow — as approved artifacts frozen with the API contract; Playwright later runs against them as pre-production verification.
+---
+
+# ui-frame-contract
+
+The frontend-design phase produces static **HTML frames** of the expected UI before any feature-UI is implemented. These approved frames are part of the contract freeze (alongside the OpenAPI/event contract) — the visual source of truth the implementation is checked against, so visual/structural drift is caught against a frozen artifact rather than discovered after the fact.
+
+## When
+The frontend-design phase, **before** feature-UI implementation. Authored by `frontend-engineer`; part of the contract freeze the parallel fan-out depends on (the gate). Pairs with `api-contract-first` and `frontend-design`.
+
+## What a frame is
+A static, self-contained **HTML file** rendering the expected UI of one screen, **design-system-first** — it links the design system's stylesheet and uses only DS tokens/styles, **no raw hex/rgb, raw px spacing, or one-off type** (consistent with `design-system-conformance`). A feature may be a **single page** or an **ordered SEQUENCE** of frames demonstrating the process/flow — e.g. login → create-org → billing → checkout. The sequence shows the flow, not just isolated screens.
+
+## Where they live + manifest
+Frames live at **`design/frames//*.html`** plus **`design/frames//manifest.json`**. The manifest holds an ordered `frames` array plus top-level approval fields:
+
+```json
+{
+ "approved": true,
+ "approvedBy": "",
+ "approvedAt": "",
+ "frames": [
+ { "id": "login", "file": "01-login.html", "title": "Sign in", "route": "/login", "acceptanceNotes": "..." }
+ ]
+}
+```
+
+The **`approved: true`** marker (with approver + date) is what **freezes** the frames.
+
+## Procedure
+
+1. **Derive the screens/states** from the user story — the pages and the order of the flow.
+2. **Build the frame(s) design-system-first** at `design/frames//*.html`, linking the DS stylesheet; write the ordered `manifest.json`.
+3. **Review and approve** — set the approval marker (`approved: true` + `approvedBy` + `approvedAt`).
+4. **Freeze WITH the contract PR.** `contract-designer`'s frozen contract **includes** the approved frames; the contract PR is **not a valid gate** until the frames exist and are approved.
+
+## Playwright against frames
+`frontend-test-engineer` runs **Playwright against the approved frames** for visual/structural assertions as **pre-production** verification — in **addition** to the built app and the live app. The frames are the visual source of truth the implementation is checked against, not a replacement for app/live e2e.
+
+## Done checklist
+- [ ] frame(s) at `design/frames//*.html`, design-system-first (no raw values)
+- [ ] single page or ordered SEQUENCE covering the flow
+- [ ] `manifest.json` with ordered `frames` (id/file/title/route/acceptanceNotes)
+- [ ] approval marker set (`approved: true` + approver + date)
+- [ ] frozen WITH the contract PR (`contract-designer`'s gate includes them)
+- [ ] `frontend-test-engineer` can run Playwright against the frames pre-production
diff --git a/.claude/skills/ui-runtime-validation/SKILL.md b/.claude/skills/ui-runtime-validation/SKILL.md
new file mode 100644
index 0000000..cafe9ef
--- /dev/null
+++ b/.claude/skills/ui-runtime-validation/SKILL.md
@@ -0,0 +1,109 @@
+---
+name: ui-runtime-validation
+description: Use when validating any UI work — the mandatory family gate that a change is rendered in a real Chromium via the Chrome DevTools MCP and the console is clean, before any UI agent reports done. Encodes the policy, the runtime gotchas, the MCP capability map, and the DONE-report gate; defers the mechanics to the chrome-devtools plugin skills. Applies to frontend-engineer (dev-time self-check), frontend-test-engineer (independent QA), and mobile-app-engineer (packaged PWA/native shell).
+---
+
+# UI runtime validation — the console-clean gate
+
+A UI that type-checks, passes unit tests, and matches a screenshot can still be **broken at
+runtime** — an uncaught exception in an effect, a 404 on a JS chunk, a CSP violation blocking an
+inline script, a mixed-content block under TLS, a failed Module-Federation remote load, a
+service-worker registration failure in a packaged PWA. **None of those show up in unit tests or a
+static frame diff.** The only way to catch them is to render the app in a real browser and read the
+console + network panels.
+
+## The rule (non-negotiable)
+
+**No UI work is "done" or "verified" until it has been loaded in a real Chromium via the Chrome
+DevTools MCP and the console is clean** — zero errors, or every remaining message understood and
+justified. This is a hard gate, not a nice-to-have. It applies to the implementer's own change
+(dev-time self-check) *and* to independent QA.
+
+- **`frontend-engineer`** — dev-time **self-validation**: render your own change (desktop and
+ small-screen/responsive) and confirm a clean console before you report `SCOPE DONE`.
+ `tools: "*"` already grants the MCP.
+- **`frontend-test-engineer`** — independent **QA**: the same console/network gate on top of the
+ Playwright acceptance + device-conformance run, on both the built app and (post-deploy) the live
+ app. A runtime error the implementer missed is a **valid, valuable bug to REPORT and ticket**
+ (route to `frontend-engineer` via the `ticket-creator` bug template) — never patched by QA,
+ never rounded up to a pass.
+- **`mobile-app-engineer`** — validate the **packaged** artifact: the installed PWA / native-shell
+ WebView must render with a clean console (service-worker registration + offline path, no CSP or
+ mixed-content breakage inside the WebView, native-plugin bridge errors).
+
+These uses are complementary, not duplicate: the implementer catches their own runtime breakage
+early; the independent verifier confirms it on the real, rendered app; the packager confirms it
+survives the PWA/native shell.
+
+## The MCP + its capabilities (know the full toolset)
+
+The plugin is `chrome-devtools-mcp` (marketplace `chrome-devtools-plugins`); its tools are
+`mcp__plugin_chrome-devtools-mcp_chrome-devtools__*` and it drives a real Chromium (Puppeteer).
+Be familiar with the whole surface and reach for the right tool — not only the console:
+
+| Need | MCP tool | Plugin skill for how-to |
+|------|----------|--------------------------|
+| Uncaught errors / warnings / CSP / mixed-content | `list_console_messages`, `get_console_message` | `chrome-devtools` |
+| Failed / cross-origin / 4xx-5xx requests, chunk & remote loads | `list_network_requests`, `get_network_request` | `chrome-devtools` |
+| Drive the app (click/type/navigate/wait) | `click`, `fill`, `fill_form`, `type_text`, `navigate_page`, `wait_for` | `chrome-devtools` |
+| Accessibility / DOM tree audit | `take_snapshot`, `lighthouse_audit` | `a11y-debugging` |
+| Performance / Core Web Vitals / LCP | `performance_start_trace`, `performance_stop_trace`, `performance_analyze_insight`, `lighthouse_audit` | `debug-optimize-lcp` |
+| Mobile / responsive viewport | `emulate`, `resize_page` | `chrome-devtools` |
+| Memory leaks / OOM | `take_heapsnapshot` | `memory-leak-debugging` |
+| Visual evidence | `take_screenshot` | `chrome-devtools` |
+| Server won't connect / no target | — | `troubleshooting` |
+
+The `chrome-devtools` and `a11y-debugging` skills carry the detailed mechanics; this skill is the
+**family policy** that wraps them.
+
+## Procedure
+
+1. **Serve the UI.** Point the browser at the running surface: the ephemeral stack (kind +
+ version-pinned base services), the local dev server, the approved static frames
+ (`design/frames//`), the installed PWA, or — post-deploy — the live app.
+2. **Navigate** to every route/state the change touches — including empty, loading, and error
+ states, and each frame in a flow sequence; validate desktop **and** a small-screen viewport
+ under device emulation.
+3. **Read the console** (`list_console_messages`). Enumerate **all** messages.
+4. **Read the network panel** (`list_network_requests`) — no failed or unexpectedly cross-origin
+ app requests.
+5. **Reproduce interactions** — click/type through the primary user actions and **re-check the
+ console after each**; many errors only fire on interaction, not initial paint.
+6. **Capture evidence** — screenshot + a console/network summary. This is the artifact that backs
+ the `SCOPE DONE` claim (and the trace attached to a filed bug).
+
+## Runtime failures to catch
+
+- **CSP violations & mixed-content under TLS.** The family standard is a **same-origin API base**
+ under TLS — an absolute `http://` API host, or an inline-script CSP breach, surfaces here as a
+ blocked request / console violation. This is the most common runtime regression.
+- **Module-Federation remote-load failures** — `ScriptExternalLoadError`, shared-singleton version
+ mismatch (two Reacts), a remote 404. The host shell mounts federated remotes; a broken remote is
+ a blank panel, not a build error.
+- **Framework runtime warnings that are real bugs** — key warnings, invalid DOM nesting, state
+ update on an unmounted component, hydration mismatch.
+- **Packaged-shell failures (PWA / native)** — service-worker registration/scope errors, offline
+ route failures, CSP tightened by the WebView, native-plugin bridge exceptions.
+
+## What counts as a clean pass
+
+- **Zero** `error`-level console messages and **zero** uncaught exceptions / unhandled rejections
+ across every route+state exercised.
+- **Zero** CSP / mixed-content violations.
+- **Zero** failed (4xx/5xx/blocked) or wrongly cross-origin network requests for app-owned resources.
+- Any surviving `warn`/`info` message is **explicitly explained** in the report — an unexplained
+ warning is a flagged gap, not a pass.
+
+## Reporting (fold into the mandatory DONE report)
+
+- **`frontend-engineer` / `mobile-app-engineer`** → under `SCOPE DONE (verified)`: e.g. "Chrome
+ DevTools MCP render of `` (desktop + small-screen / packaged shell): console clean — 0
+ errors, 0 CSP/mixed-content, 0 failed requests" — or list every remaining message with its
+ justification.
+- **`frontend-test-engineer`** → the console/network inspection result **per acceptance criterion**,
+ for both pre-production (built app / approved frames) and post-production (live app), plus the
+ **Jira bug key** for any runtime defect found.
+
+Never report a UI slice as verified on the strength of unit tests or a screenshot alone. If the
+browser could not be driven (environment blocked), say so and RETURN `BLOCKED:` — a skipped console
+check is a flagged gap, **never** a silent pass.
diff --git a/.claude/skills/verification-protocol/SKILL.md b/.claude/skills/verification-protocol/SKILL.md
new file mode 100644
index 0000000..ecdd3bd
--- /dev/null
+++ b/.claude/skills/verification-protocol/SKILL.md
@@ -0,0 +1,26 @@
+---
+name: verification-protocol
+description: Use before claiming any code/PR work is done. The push/PR verification discipline every code-producing agent follows: confirm the remote SHA, confirm the PR via API, and report the honest done-contract.
+---
+
+# verification-protocol
+
+Evidence before assertions. Never claim done from local state alone.
+
+## Steps
+1. **Environment sanity** — confirm you are in the right repo/branch (`git remote -v`, `git rev-parse --abbrev-ref HEAD`).
+2. **Push is real** — after pushing, confirm the remote moved: `git ls-remote origin ` shows your new SHA (not the local ref).
+3. **PR is real** — confirm via API, not a guessed URL: `gh pr view -R --json url,state,headRefOid`.
+4. **Checks** — `gh pr checks ` / `gh run view` for the actual conclusions; quote them.
+5. **Signatures** — for protected repos, confirm the merge/commit is `verified` (`gh api repos//commits/ --jq .commit.verification.verified`).
+
+## Done-contract (mandatory output)
+`SCOPE DONE (verified): ` and `OUT OF SCOPE — NOT DONE: `. A failing result reported honestly is a valid deliverable; a green claim you did not verify is not.
+
+## Verification under the model cascade
+
+When work runs through the tiered cascade (`model-cascade` skill / `governance/model-cascade.md`):
+
+- **Verification flows up, never down.** A parent verifies each child against the spec it handed down; a child never self-certifies. **A lower tier never grades a higher tier's work** — verification only moves up the tree (or sideways to an independent equal/higher lane).
+- **The completeness verdict is a fresh, higher tier.** The final "is the whole thing done" pass is an independent **Opus** run on **fresh context**, returning **PASS / GAPS / FAIL** against the *original* scope. It is *additional* to this push/PR discipline, the CI Harden Gate, and the QA lanes — not a substitute for any of them. Only the orchestrator declares the *feature* done.
+- **`ESCALATE:` vs `BLOCKED:`.** A node that exceeds its tier returns **`ESCALATE: `** — the **up-a-tier** (machine) sibling of **`BLOCKED: `**, which reaches a **human** for a decision/credential/input. Never guess past your competence (`ESCALATE`); never stall an async run waiting on a human (push, then `BLOCKED`).
diff --git a/.fuze/identifier-allowlist.txt b/.fuze/identifier-allowlist.txt
new file mode 100644
index 0000000..0367af5
--- /dev/null
+++ b/.fuze/identifier-allowlist.txt
@@ -0,0 +1,25 @@
+# identifier-allowlist — gate-identifier exemptions (governance/identifier-standard.md §6)
+#
+# List create operations that are EXEMPT from the server-owned identifier rule
+# because the client legitimately assigns the id — an offline-first surface that
+# cannot round-trip for one, or a sync protocol where the client is a trusted
+# peer rather than an anonymous caller.
+#
+# Prefer annotating the OpenAPI operation with `x-client-assigned-id: allowed`
+# (+ `x-client-assigned-id-reason`) instead; use this file only when you cannot
+# edit the operation object (e.g. a vendored/generated spec).
+#
+# An exemption is not a waiver of the type-confusion defense. An exempted
+# operation MUST still either derive the stored id as
+# `uuidv5(, clientRequestId)` or enforce a tenant-scoped
+# `(tenant_id, id)` primary key, so a chosen id cannot collide across types or
+# cross a tenant boundary. See §6.
+#
+# Format:
+# - one route per line, as `METHOD /path` (matched case-insensitively)
+# - `#` begins a comment; blank lines are ignored
+#
+# Example (remove — illustrative only):
+# POST /v1/sync/documents # offline-first editor; ids are uuidv5-namespaced
+#
+# (Empty by default — no exemptions.)
diff --git a/.fuze/installed.json b/.fuze/installed.json
new file mode 100644
index 0000000..2b213af
--- /dev/null
+++ b/.fuze/installed.json
@@ -0,0 +1,924 @@
+{
+ "schemaVersion": 1,
+ "files": {
+ ".claude/agents/a2a-maintainer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/a2a-maintainer.md",
+ "sourceSha256": "07663ea2678e6bf142fe2669399c1a80bb50ebbe555a78e4744177ac0e23bd4c",
+ "renderedSha256": "07663ea2678e6bf142fe2669399c1a80bb50ebbe555a78e4744177ac0e23bd4c"
+ },
+ ".claude/agents/agile-manager.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/agile-manager.md",
+ "sourceSha256": "d530d5adfcda0b6333d62fa6b3227678438a4a16ce174df2a3647bb2b4170760",
+ "renderedSha256": "d530d5adfcda0b6333d62fa6b3227678438a4a16ce174df2a3647bb2b4170760"
+ },
+ ".claude/agents/backend-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/backend-engineer.md",
+ "sourceSha256": "eefbb85cf686a6c0a0ecc8623af90ed2244c11821a5d033ece48a77ba3ee9868",
+ "renderedSha256": "eefbb85cf686a6c0a0ecc8623af90ed2244c11821a5d033ece48a77ba3ee9868"
+ },
+ ".claude/agents/cli-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/cli-engineer.md",
+ "sourceSha256": "6f9c3d4985dba926fe9e8f5f6279fcbe37fd43b9a552b640a366b82c46066566",
+ "renderedSha256": "6f9c3d4985dba926fe9e8f5f6279fcbe37fd43b9a552b640a366b82c46066566"
+ },
+ ".claude/agents/contract-designer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/contract-designer.md",
+ "sourceSha256": "5623bf51e3eb1b5a45d3b0af4d0b32b618a7ac3a4bb1332ec09e7bcdc4e44b86",
+ "renderedSha256": "5623bf51e3eb1b5a45d3b0af4d0b32b618a7ac3a4bb1332ec09e7bcdc4e44b86"
+ },
+ ".claude/agents/database-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/database-engineer.md",
+ "sourceSha256": "0eee2813a846910b220c4c586f910571750dbcea0ccf9622491a9644f0636dab",
+ "renderedSha256": "0eee2813a846910b220c4c586f910571750dbcea0ccf9622491a9644f0636dab"
+ },
+ ".claude/agents/devops-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/devops-engineer.md",
+ "sourceSha256": "282f60005144669906d7b96bc2e534f67d4e3db8f5df05e1a7b04baf47083be1",
+ "renderedSha256": "282f60005144669906d7b96bc2e534f67d4e3db8f5df05e1a7b04baf47083be1"
+ },
+ ".claude/agents/docs-maintainer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/docs-maintainer.md",
+ "sourceSha256": "62581d7936a330055acc455d94f08fbe9ae97aa3f5023633f96062a23a5be01e",
+ "renderedSha256": "62581d7936a330055acc455d94f08fbe9ae97aa3f5023633f96062a23a5be01e"
+ },
+ ".claude/agents/frontend-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/frontend-engineer.md",
+ "sourceSha256": "c6e35d44bd6768c6d5969546d76a668e4ba3ef62d175763b25ac96a5b3d71ce4",
+ "renderedSha256": "c6e35d44bd6768c6d5969546d76a668e4ba3ef62d175763b25ac96a5b3d71ce4"
+ },
+ ".claude/agents/frontend-test-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/frontend-test-engineer.md",
+ "sourceSha256": "852dc7ee0010c5b76e7fa00a553502e61acf227f28d02866a16bfc1cccd17e4d",
+ "renderedSha256": "852dc7ee0010c5b76e7fa00a553502e61acf227f28d02866a16bfc1cccd17e4d"
+ },
+ ".claude/agents/mcp-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/mcp-engineer.md",
+ "sourceSha256": "c5d1abab36bee16e53735c4e97296af6da570a5ed504ea6f8ab4bfde67156675",
+ "renderedSha256": "c5d1abab36bee16e53735c4e97296af6da570a5ed504ea6f8ab4bfde67156675"
+ },
+ ".claude/agents/security.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/security.md",
+ "sourceSha256": "ab941e989cab87edbd623fd5b9bf912c1e2531cd7d80ea4e06bd17efc6919f33",
+ "renderedSha256": "ab941e989cab87edbd623fd5b9bf912c1e2531cd7d80ea4e06bd17efc6919f33"
+ },
+ ".claude/agents/test-engineer.md": {
+ "capability": "core-agents",
+ "mode": "managed",
+ "sourcePath": "agents/test-engineer.md",
+ "sourceSha256": "07d19c4cd581e9a680014d8aceaf5ca02b70c6c2cd156b71a422be7abed69de1",
+ "renderedSha256": "07d19c4cd581e9a680014d8aceaf5ca02b70c6c2cd156b71a422be7abed69de1"
+ },
+ ".claude/skills/api-contract-first/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/api-contract-first/SKILL.md",
+ "sourceSha256": "83f269decf84233f7ac91f7c05340737a13752d1c5b334cbd88df23a5e2d9300",
+ "renderedSha256": "83f269decf84233f7ac91f7c05340737a13752d1c5b334cbd88df23a5e2d9300"
+ },
+ ".claude/skills/design-system-conformance/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/design-system-conformance/SKILL.md",
+ "sourceSha256": "7d1950ec4c070b26affc6f462b6111ff045802fec0d87d972370059b6b75f9e9",
+ "renderedSha256": "7d1950ec4c070b26affc6f462b6111ff045802fec0d87d972370059b6b75f9e9"
+ },
+ ".claude/skills/design-system-inheritance/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/design-system-inheritance/SKILL.md",
+ "sourceSha256": "507f39db4a2a4a759794464a7b653d7eb6c5f9ce12340d74f3f21ac1429c9e04",
+ "renderedSha256": "507f39db4a2a4a759794464a7b653d7eb6c5f9ce12340d74f3f21ac1429c9e04"
+ },
+ ".claude/skills/doc-validity/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/doc-validity/SKILL.md",
+ "sourceSha256": "e7d0d4c99895edf3fd604fa8878b39e6554165501765c74e573ef1b6e2740210",
+ "renderedSha256": "e7d0d4c99895edf3fd604fa8878b39e6554165501765c74e573ef1b6e2740210"
+ },
+ ".claude/skills/feature-flags/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/feature-flags/SKILL.md",
+ "sourceSha256": "e03f51ed05aa8bb50287a20d10a307cc3a23970173c123bfd3673a92b7346009",
+ "renderedSha256": "e03f51ed05aa8bb50287a20d10a307cc3a23970173c123bfd3673a92b7346009"
+ },
+ ".claude/skills/feature-tech-planning/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/feature-tech-planning/SKILL.md",
+ "sourceSha256": "7b0797dc973404c94d02a5d57258f8befb7b490f7643f9b01619eed1e83c9127",
+ "renderedSha256": "7b0797dc973404c94d02a5d57258f8befb7b490f7643f9b01619eed1e83c9127"
+ },
+ ".claude/skills/fuzefront-ui-package/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/fuzefront-ui-package/SKILL.md",
+ "sourceSha256": "772c7031eca212e59f8509b8a4e773c975aeb5f63b6595d549c96f9c127af600",
+ "renderedSha256": "772c7031eca212e59f8509b8a4e773c975aeb5f63b6595d549c96f9c127af600"
+ },
+ ".claude/skills/logging/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/logging/SKILL.md",
+ "sourceSha256": "b80bd1a00ad2ad001c357ad66d486bffa6b8d691a04a69c56d8ccd810b854f4b",
+ "renderedSha256": "b80bd1a00ad2ad001c357ad66d486bffa6b8d691a04a69c56d8ccd810b854f4b"
+ },
+ ".claude/skills/managed-agents-roles/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/managed-agents-roles/SKILL.md",
+ "sourceSha256": "888ffd0b73f3233f27f9b60b3ca5415fba65399ee6a471571a6be98f43e61437",
+ "renderedSha256": "888ffd0b73f3233f27f9b60b3ca5415fba65399ee6a471571a6be98f43e61437"
+ },
+ ".claude/skills/mobile-conformance/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/mobile-conformance/SKILL.md",
+ "sourceSha256": "476983a3b24b634d1a8c0e33e7475d4053c31ed20ac1a2ccf6bea2cc3c362046",
+ "renderedSha256": "476983a3b24b634d1a8c0e33e7475d4053c31ed20ac1a2ccf6bea2cc3c362046"
+ },
+ ".claude/skills/model-cascade/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/model-cascade/SKILL.md",
+ "sourceSha256": "1673f06d41c25b894fc587e55bd02da02e338bbc3d9eee4d69a3b5031c3f1913",
+ "renderedSha256": "1673f06d41c25b894fc587e55bd02da02e338bbc3d9eee4d69a3b5031c3f1913"
+ },
+ ".claude/skills/repo-hardening/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/repo-hardening/SKILL.md",
+ "sourceSha256": "b33d38d40d677f2f62385971efb9ba7401a47aa22cf9d642ef70854b56cf688b",
+ "renderedSha256": "b33d38d40d677f2f62385971efb9ba7401a47aa22cf9d642ef70854b56cf688b"
+ },
+ ".claude/skills/service-cli/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/service-cli/SKILL.md",
+ "sourceSha256": "aab3b6e29248ef9bfef2f6bd30924bd3a201cb9b30cc81cf6e0671ead75d8375",
+ "renderedSha256": "aab3b6e29248ef9bfef2f6bd30924bd3a201cb9b30cc81cf6e0671ead75d8375"
+ },
+ ".claude/skills/ticket-creator/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/SKILL.md",
+ "sourceSha256": "401d94c0ff9a99acabdfc6de19a0338c9a2337abf53b5bf13a6b39eca7e05e78",
+ "renderedSha256": "401d94c0ff9a99acabdfc6de19a0338c9a2337abf53b5bf13a6b39eca7e05e78"
+ },
+ ".claude/skills/ticket-creator/backend/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/backend/SKILL.md",
+ "sourceSha256": "f4254bc5a8cda575acdee84bbb8769fc353e5a60fcd90a369f3ee099805331b0",
+ "renderedSha256": "f4254bc5a8cda575acdee84bbb8769fc353e5a60fcd90a369f3ee099805331b0"
+ },
+ ".claude/skills/ticket-creator/bug/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/bug/SKILL.md",
+ "sourceSha256": "bb5faa8e5b9aa8b627c65bce935bbe73df4d95c28af63589a48bd76e18fbbd42",
+ "renderedSha256": "bb5faa8e5b9aa8b627c65bce935bbe73df4d95c28af63589a48bd76e18fbbd42"
+ },
+ ".claude/skills/ticket-creator/devops/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/devops/SKILL.md",
+ "sourceSha256": "9c6d38d7f96ff200c8ca8217a3de3850d2ac2945728a0a16199e0fc318baa84d",
+ "renderedSha256": "9c6d38d7f96ff200c8ca8217a3de3850d2ac2945728a0a16199e0fc318baa84d"
+ },
+ ".claude/skills/ticket-creator/docs/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/docs/SKILL.md",
+ "sourceSha256": "7ef59d846b605c5a3ff8362a700d3d88fca62365bfb03bd4e3761994a1e63bd4",
+ "renderedSha256": "7ef59d846b605c5a3ff8362a700d3d88fca62365bfb03bd4e3761994a1e63bd4"
+ },
+ ".claude/skills/ticket-creator/epic/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/epic/SKILL.md",
+ "sourceSha256": "9e66e9e76a2209b1c81f1086389f02dc8c9187f9e1418f5b5e27d49cf3db94b5",
+ "renderedSha256": "9e66e9e76a2209b1c81f1086389f02dc8c9187f9e1418f5b5e27d49cf3db94b5"
+ },
+ ".claude/skills/ticket-creator/frontend/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/frontend/SKILL.md",
+ "sourceSha256": "b4400c8aabfac295364fe1e326adfe0085eef35ff772b9f10d46a28e42764899",
+ "renderedSha256": "b4400c8aabfac295364fe1e326adfe0085eef35ff772b9f10d46a28e42764899"
+ },
+ ".claude/skills/ticket-creator/qa/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/qa/SKILL.md",
+ "sourceSha256": "020e1c3a40efa097f9dc883137a0b9937ddc68f7cc32da739957f2b3dbcd3d92",
+ "renderedSha256": "020e1c3a40efa097f9dc883137a0b9937ddc68f7cc32da739957f2b3dbcd3d92"
+ },
+ ".claude/skills/ticket-creator/qa/functional/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/qa/functional/SKILL.md",
+ "sourceSha256": "6f9016fec42a5cd3779b282f488188342b8d8f890e9f5191c52ab76bf7a648bd",
+ "renderedSha256": "6f9016fec42a5cd3779b282f488188342b8d8f890e9f5191c52ab76bf7a648bd"
+ },
+ ".claude/skills/ticket-creator/qa/integration/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/qa/integration/SKILL.md",
+ "sourceSha256": "7cb4ebfc0736c56f61b382a08bbf9ec02932eba8dc78c700b1e1a0c61c6d6f31",
+ "renderedSha256": "7cb4ebfc0736c56f61b382a08bbf9ec02932eba8dc78c700b1e1a0c61c6d6f31"
+ },
+ ".claude/skills/ticket-creator/qa/load-stress/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/qa/load-stress/SKILL.md",
+ "sourceSha256": "7e492d0c0a6bc951d883761df01ba2b01301886a738d521a36b25f7fe3a6af93",
+ "renderedSha256": "7e492d0c0a6bc951d883761df01ba2b01301886a738d521a36b25f7fe3a6af93"
+ },
+ ".claude/skills/ticket-creator/qa/security/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/qa/security/SKILL.md",
+ "sourceSha256": "2bbe07ad8f27c5ec318f4cf712055dd281bd21140b2f8055cfb044c99c6b179a",
+ "renderedSha256": "2bbe07ad8f27c5ec318f4cf712055dd281bd21140b2f8055cfb044c99c6b179a"
+ },
+ ".claude/skills/ticket-creator/qa/unit/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/qa/unit/SKILL.md",
+ "sourceSha256": "4201415d62fc24cd9b5bfb521798017a4e74c92fd5cb7f1577c3c001e7ca3268",
+ "renderedSha256": "4201415d62fc24cd9b5bfb521798017a4e74c92fd5cb7f1577c3c001e7ca3268"
+ },
+ ".claude/skills/ticket-creator/story/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/story/SKILL.md",
+ "sourceSha256": "737bd59465e739035b287b5ebe144a511c097871f6ce68c09141da0d5dfa5cf8",
+ "renderedSha256": "737bd59465e739035b287b5ebe144a511c097871f6ce68c09141da0d5dfa5cf8"
+ },
+ ".claude/skills/ticket-creator/ux/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-creator/ux/SKILL.md",
+ "sourceSha256": "1f6aeca9a59c029a3cd9c5c3ff865ef1cede51488cddfbee65092684315a54b6",
+ "renderedSha256": "1f6aeca9a59c029a3cd9c5c3ff865ef1cede51488cddfbee65092684315a54b6"
+ },
+ ".claude/skills/ticket-enforcer/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-enforcer/SKILL.md",
+ "sourceSha256": "58b936c8670162d3f6281e9b960f98ab6223937a5aac12c2e63d88dcaf6b6e7a",
+ "renderedSha256": "58b936c8670162d3f6281e9b960f98ab6223937a5aac12c2e63d88dcaf6b6e7a"
+ },
+ ".claude/skills/ticket-reviewer/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/SKILL.md",
+ "sourceSha256": "99ebdc75bbc5eae409cc35b147ba04dbc36979bd6512ae448dd8c5d8d0e7e675",
+ "renderedSha256": "99ebdc75bbc5eae409cc35b147ba04dbc36979bd6512ae448dd8c5d8d0e7e675"
+ },
+ ".claude/skills/ticket-reviewer/bug/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/bug/SKILL.md",
+ "sourceSha256": "3ec912e90d2da19eaa68917817f48d786b59b3cb0b6183c49bce81d4689ec5f5",
+ "renderedSha256": "3ec912e90d2da19eaa68917817f48d786b59b3cb0b6183c49bce81d4689ec5f5"
+ },
+ ".claude/skills/ticket-reviewer/dev-task/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/dev-task/SKILL.md",
+ "sourceSha256": "f3984f390e7acfbc50320c4ddb554640e9cfae28cd397e12bf348516067daa9b",
+ "renderedSha256": "f3984f390e7acfbc50320c4ddb554640e9cfae28cd397e12bf348516067daa9b"
+ },
+ ".claude/skills/ticket-reviewer/devops/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/devops/SKILL.md",
+ "sourceSha256": "d72c24d9f49fa3d4522210f9a32f0a12c2efe9cd18e2f35d455b69f4840c0590",
+ "renderedSha256": "d72c24d9f49fa3d4522210f9a32f0a12c2efe9cd18e2f35d455b69f4840c0590"
+ },
+ ".claude/skills/ticket-reviewer/docs/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/docs/SKILL.md",
+ "sourceSha256": "aa98b1c9983209cd7305984462cb363174e5ba9f7ddbdb9570b6472723161b85",
+ "renderedSha256": "aa98b1c9983209cd7305984462cb363174e5ba9f7ddbdb9570b6472723161b85"
+ },
+ ".claude/skills/ticket-reviewer/epic/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/epic/SKILL.md",
+ "sourceSha256": "1f414cccadd1e84f1a5947be3fd043911b98f04a3140bb301983dd35e708f008",
+ "renderedSha256": "1f414cccadd1e84f1a5947be3fd043911b98f04a3140bb301983dd35e708f008"
+ },
+ ".claude/skills/ticket-reviewer/qa/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/qa/SKILL.md",
+ "sourceSha256": "394f95a4cfcfe776a1ea65f47f0583bcd71f20cdad96a7eb8d17e8c2216d9608",
+ "renderedSha256": "394f95a4cfcfe776a1ea65f47f0583bcd71f20cdad96a7eb8d17e8c2216d9608"
+ },
+ ".claude/skills/ticket-reviewer/story/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ticket-reviewer/story/SKILL.md",
+ "sourceSha256": "a832ba5f191a5b3277fb9b0b4c09343fe5b7363104adf8eb960f132f7297a722",
+ "renderedSha256": "a832ba5f191a5b3277fb9b0b4c09343fe5b7363104adf8eb960f132f7297a722"
+ },
+ ".claude/skills/ui-frame-contract/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ui-frame-contract/SKILL.md",
+ "sourceSha256": "42f37f74cc1d3e81feb72c2fdf123a40e4a90f1bc30045649fbadf4ac932f92d",
+ "renderedSha256": "42f37f74cc1d3e81feb72c2fdf123a40e4a90f1bc30045649fbadf4ac932f92d"
+ },
+ ".claude/skills/ui-runtime-validation/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/ui-runtime-validation/SKILL.md",
+ "sourceSha256": "9a025e7dec7ddd21b72374676adea53d3e16136d4f6fec735f6b2b778540ec10",
+ "renderedSha256": "9a025e7dec7ddd21b72374676adea53d3e16136d4f6fec735f6b2b778540ec10"
+ },
+ ".claude/skills/verification-protocol/SKILL.md": {
+ "capability": "skills",
+ "mode": "managed",
+ "sourcePath": "skills/verification-protocol/SKILL.md",
+ "sourceSha256": "de74336a097829dbc6019af52f3f192b62339272935413e718b3917f42e5b3db",
+ "renderedSha256": "de74336a097829dbc6019af52f3f192b62339272935413e718b3917f42e5b3db"
+ },
+ ".fuze/identifier-allowlist.txt": {
+ "capability": "entity-identity",
+ "mode": "seeded",
+ "sourcePath": "governance/identifier-allowlist.txt",
+ "sourceSha256": "e63c72829872d7e7a42b1766bf161b6f7dec119b929b217a8333e3f6440dc883",
+ "renderedSha256": "e63c72829872d7e7a42b1766bf161b6f7dec119b929b217a8333e3f6440dc883"
+ },
+ ".fuze/repo-manifest.schema.json": {
+ "capability": "schema-validate",
+ "mode": "managed",
+ "sourcePath": "governance/repo-manifest.schema.json",
+ "sourceSha256": "0609a29df35b0adb8fbda5c15a695a693bc09fb6d01b40bc19b933d23fc249d5",
+ "renderedSha256": "0609a29df35b0adb8fbda5c15a695a693bc09fb6d01b40bc19b933d23fc249d5"
+ },
+ ".github/labels.json": {
+ "capability": "community",
+ "mode": "seeded",
+ "sourcePath": "community-templates/labels.json",
+ "sourceSha256": "e179fa24088c360b368cf468d5974d1813029229d9f08f4127892eb4c15c5bb5",
+ "renderedSha256": "e179fa24088c360b368cf468d5974d1813029229d9f08f4127892eb4c15c5bb5"
+ },
+ ".github/workflows/a2a-maintain.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/a2a-maintain.yml",
+ "sourceSha256": "35d9e4cf7b06623fa892cfc36fd440c4b19d633dacfc9d61c0362cb0383ce77b",
+ "renderedSha256": "35d9e4cf7b06623fa892cfc36fd440c4b19d633dacfc9d61c0362cb0383ce77b",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/auto-merge.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/auto-merge.yml",
+ "sourceSha256": "2d2d09380b72e9d56725a137205bc7351e91dc9feb20be41120d6e418d8b4027",
+ "renderedSha256": "2d2d09380b72e9d56725a137205bc7351e91dc9feb20be41120d6e418d8b4027",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/claude-auto-pr.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/claude-auto-pr.yml",
+ "sourceSha256": "7d0fd50f72145abe4225120998b68bb23a5a79dee8a7d93cbe7f94730ea5e354",
+ "renderedSha256": "7d0fd50f72145abe4225120998b68bb23a5a79dee8a7d93cbe7f94730ea5e354",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/claude-ci-autofix.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/claude-ci-autofix.yml",
+ "sourceSha256": "35da8c61ab208185aadb47d3e70b31d782bde856e3804d64ef574cc7bcefe0c7",
+ "renderedSha256": "35da8c61ab208185aadb47d3e70b31d782bde856e3804d64ef574cc7bcefe0c7",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/claude.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/claude.yml",
+ "sourceSha256": "f79807ed2e35d8bc6e0dc48a5697c3537e4ebe33ffe33212c15571aec6285aff",
+ "renderedSha256": "f79807ed2e35d8bc6e0dc48a5697c3537e4ebe33ffe33212c15571aec6285aff",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/design-approval.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/design-approval.yml",
+ "sourceSha256": "ee6fa15c16a2c5df69e184e9817da9a80fd7a68cff9f9d9cc23c6b4da376040f",
+ "renderedSha256": "ee6fa15c16a2c5df69e184e9817da9a80fd7a68cff9f9d9cc23c6b4da376040f",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/design-review-notify.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/design-review-notify.yml",
+ "sourceSha256": "31efd5b3f9c6239dadf2a719eb7bebf413a02bb49580fae6cec60e1345ff1e71",
+ "renderedSha256": "31efd5b3f9c6239dadf2a719eb7bebf413a02bb49580fae6cec60e1345ff1e71",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/gate-frames-stamped.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/gate-frames-stamped.yml",
+ "sourceSha256": "69fd04a77f3dc79a15756aa43bc1b4cd2ae760952b6b6d85e6846cd4b81539da",
+ "renderedSha256": "69fd04a77f3dc79a15756aa43bc1b4cd2ae760952b6b6d85e6846cd4b81539da",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/gate-line-endings.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/gate-line-endings.yml",
+ "sourceSha256": "474b74ef31a3c8fceacb6a420e66d4482df4996360b8e3f123e60c3084ce8f6e",
+ "renderedSha256": "474b74ef31a3c8fceacb6a420e66d4482df4996360b8e3f123e60c3084ce8f6e",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/governance-nightly.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/governance-nightly.yml",
+ "sourceSha256": "8bc3fbb28fb307f33cf03ea04a71fb6a68c3d8e8ebd2f5920a41faa60f81343d",
+ "renderedSha256": "8bc3fbb28fb307f33cf03ea04a71fb6a68c3d8e8ebd2f5920a41faa60f81343d",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/governance-sync.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/governance-sync.yml",
+ "sourceSha256": "8fbd260435ddeb153996c0d73abcadb6459a08106b193862a9ff3821547055ba",
+ "renderedSha256": "8fbd260435ddeb153996c0d73abcadb6459a08106b193862a9ff3821547055ba",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/harden-gate.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/harden-gate.yml",
+ "sourceSha256": "2bacc78f5db4eedc4482f79dffb010c20cab9f2577c3a25afd71de99868d8c70",
+ "renderedSha256": "2bacc78f5db4eedc4482f79dffb010c20cab9f2577c3a25afd71de99868d8c70",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/helm-validate.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/helm-validate.yml",
+ "sourceSha256": "ce2755ddb8bbc0649e84a9e51fb406f1936f2775c4310145124ff78432520fbf",
+ "renderedSha256": "ce2755ddb8bbc0649e84a9e51fb406f1936f2775c4310145124ff78432520fbf",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/label-sync.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/label-sync.yml",
+ "sourceSha256": "e9b72448e00bf4c7c04ac23fc03d0b10f7aa2f225b9d410667e1ffac5a493362",
+ "renderedSha256": "e9b72448e00bf4c7c04ac23fc03d0b10f7aa2f225b9d410667e1ffac5a493362",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/mcp-maintain.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/mcp-maintain.yml",
+ "sourceSha256": "5fb529630898bc1bdc0163d251ec4f2c650b544342bac2c9fe43920775d1759d",
+ "renderedSha256": "5fb529630898bc1bdc0163d251ec4f2c650b544342bac2c9fe43920775d1759d",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/nightly-integration.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/nightly-integration.yml",
+ "sourceSha256": "6efaff8acf9a32a236f81da8a9beee5b05a5a7226d9df8178ddc0be7048edea5",
+ "renderedSha256": "6efaff8acf9a32a236f81da8a9beee5b05a5a7226d9df8178ddc0be7048edea5",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/pages-frames.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/pages-frames.yml",
+ "sourceSha256": "3eb01d66ca9eb8c7b9a6541a6488542dd39b7412d18a28446aceb90e83ee4563",
+ "renderedSha256": "3eb01d66ca9eb8c7b9a6541a6488542dd39b7412d18a28446aceb90e83ee4563",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/publish-expert.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/publish-expert.yml",
+ "sourceSha256": "06027343bf44190cacbd84241ada036a6556c288411d9ed0b2da0991d35aef87",
+ "renderedSha256": "06027343bf44190cacbd84241ada036a6556c288411d9ed0b2da0991d35aef87",
+ "renderVars": {
+ "defaultBranch": "main",
+ "baselineRef": "v1"
+ }
+ },
+ ".github/workflows/telegram-pr-merged.yml": {
+ "capability": "workflows",
+ "mode": "stamped",
+ "sourcePath": "workflow-templates/telegram-pr-merged.yml",
+ "sourceSha256": "d7b161e66268ee150ccda2726176449262f608b4ee29e397051709aa4c2a3ecc",
+ "renderedSha256": "d7b161e66268ee150ccda2726176449262f608b4ee29e397051709aa4c2a3ecc"
+ },
+ ".npmrc": {
+ "capability": "npm-registry",
+ "mode": "seeded",
+ "sourcePath": "community-templates/npmrc",
+ "sourceSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc",
+ "renderedSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc"
+ },
+ ".nvmrc": {
+ "capability": "node-toolchain",
+ "mode": "seeded",
+ "sourcePath": "",
+ "sourceSha256": "68ca3fba3b7e864770cb61aeb306d4bd4354b68ab4dd38450860c5d823e42a53",
+ "renderedSha256": "68ca3fba3b7e864770cb61aeb306d4bd4354b68ab4dd38450860c5d823e42a53"
+ },
+ "agent-templates/providers/README.md": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/README.md",
+ "sourceSha256": "b0d8e9a70db32dd620ba8bbdc795231fb41dc34dafa3de1c1e322aa9e9c698e3",
+ "renderedSha256": "b0d8e9a70db32dd620ba8bbdc795231fb41dc34dafa3de1c1e322aa9e9c698e3"
+ },
+ "agent-templates/providers/__init__.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/__init__.py",
+ "sourceSha256": "27c57128919ac2b76540a16e90c09a16c0fff7254f332c54e1060a828729e7d4",
+ "renderedSha256": "27c57128919ac2b76540a16e90c09a16c0fff7254f332c54e1060a828729e7d4"
+ },
+ "agent-templates/providers/anthropic/__init__.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/anthropic/__init__.py",
+ "sourceSha256": "c1fd152a1c0758aca7b27979c0634f8d816ce99aa757bb870e4736bf82f478b0",
+ "renderedSha256": "c1fd152a1c0758aca7b27979c0634f8d816ce99aa757bb870e4736bf82f478b0"
+ },
+ "agent-templates/providers/anthropic/adapter.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/anthropic/adapter.py",
+ "sourceSha256": "0a9e17075c50772298d71f4a163c67554d8894ec7fbf612285a8343acf8f7612",
+ "renderedSha256": "0a9e17075c50772298d71f4a163c67554d8894ec7fbf612285a8343acf8f7612"
+ },
+ "agent-templates/providers/base.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/base.py",
+ "sourceSha256": "480821608c9c009aea949caca9df9701b2d88857e3aa1fc122a858ad7ad3c773",
+ "renderedSha256": "480821608c9c009aea949caca9df9701b2d88857e3aa1fc122a858ad7ad3c773"
+ },
+ "agent-templates/providers/hermes/__init__.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/hermes/__init__.py",
+ "sourceSha256": "6453fce6d954113ba0e5182543c500f010e3578162e07301a361807fd13ea0c1",
+ "renderedSha256": "6453fce6d954113ba0e5182543c500f010e3578162e07301a361807fd13ea0c1"
+ },
+ "agent-templates/providers/hermes/adapter.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/hermes/adapter.py",
+ "sourceSha256": "9fcedb97b327e5b48d960c19a943e800144de5a2b0fbd0b213be9578b76b6e70",
+ "renderedSha256": "9fcedb97b327e5b48d960c19a943e800144de5a2b0fbd0b213be9578b76b6e70"
+ },
+ "agent-templates/providers/openai/__init__.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/openai/__init__.py",
+ "sourceSha256": "bf0bc1ad3e08bf4fa29852ceb8018c7ac614797c07a65462b1c3411f6bf55567",
+ "renderedSha256": "bf0bc1ad3e08bf4fa29852ceb8018c7ac614797c07a65462b1c3411f6bf55567"
+ },
+ "agent-templates/providers/openai/adapter.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/openai/adapter.py",
+ "sourceSha256": "0bfdae63b2caf64309443bea5bd612189cca780511ef5bb1883ea4ca48387057",
+ "renderedSha256": "0bfdae63b2caf64309443bea5bd612189cca780511ef5bb1883ea4ca48387057"
+ },
+ "agent-templates/providers/provision.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/providers/provision.py",
+ "sourceSha256": "757da115f5426ce955670e49e378e2489d29e54314b9c54dfbc4176434149b2d",
+ "renderedSha256": "757da115f5426ce955670e49e378e2489d29e54314b9c54dfbc4176434149b2d"
+ },
+ "agent-templates/roles/_base/role.json": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/roles/_base/role.json",
+ "sourceSha256": "67add1adf33f093cb9ac0ef63546b623a9a11ace98789caf7d141be502a4e937",
+ "renderedSha256": "67add1adf33f093cb9ac0ef63546b623a9a11ace98789caf7d141be502a4e937"
+ },
+ "agent-templates/schema/environment.schema.json": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/schema/environment.schema.json",
+ "sourceSha256": "525b7489a4044190c40f13443012f23985cc4de0602893438057feb20a8b39f3",
+ "renderedSha256": "525b7489a4044190c40f13443012f23985cc4de0602893438057feb20a8b39f3"
+ },
+ "agent-templates/schema/memory.schema.json": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/schema/memory.schema.json",
+ "sourceSha256": "f2645779babcc1d8ac7efa3c61021c60e5e75011bb2a79cadf7a470a7f12d4ef",
+ "renderedSha256": "f2645779babcc1d8ac7efa3c61021c60e5e75011bb2a79cadf7a470a7f12d4ef"
+ },
+ "agent-templates/schema/mobile-requirements.schema.json": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/schema/mobile-requirements.schema.json",
+ "sourceSha256": "30a1e94b36bfb25ec65aecda3dac5d62c4ac7fe9153c85b4fb279728b3497fe5",
+ "renderedSha256": "30a1e94b36bfb25ec65aecda3dac5d62c4ac7fe9153c85b4fb279728b3497fe5"
+ },
+ "agent-templates/schema/role-manifest.schema.json": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/schema/role-manifest.schema.json",
+ "sourceSha256": "cdeacc70a3a55aafc8c2e7a2ae63227216b9eed38183895b42b6e2426fd16e1c",
+ "renderedSha256": "cdeacc70a3a55aafc8c2e7a2ae63227216b9eed38183895b42b6e2426fd16e1c"
+ },
+ "agent-templates/schema/vault.schema.json": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/schema/vault.schema.json",
+ "sourceSha256": "993c7415d20397688502fbc3f27ae4a564769a6f5d258216a929d55691a291be",
+ "renderedSha256": "993c7415d20397688502fbc3f27ae4a564769a6f5d258216a929d55691a291be"
+ },
+ "agent-templates/sync/.gitignore": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/.gitignore",
+ "sourceSha256": "5e7a2fb056f4ee84e115d0594961787158d521873482319df251d8ee27e0ebad",
+ "renderedSha256": "5e7a2fb056f4ee84e115d0594961787158d521873482319df251d8ee27e0ebad"
+ },
+ "agent-templates/sync/common.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/common.py",
+ "sourceSha256": "01b1126a920f5aab5be113a71b85d97b6b26eb4f727a9ce641cdd0c57158ae02",
+ "renderedSha256": "01b1126a920f5aab5be113a71b85d97b6b26eb4f727a9ce641cdd0c57158ae02"
+ },
+ "agent-templates/sync/driver.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/driver.py",
+ "sourceSha256": "b158508af21086c3a7c5ea40be3a151d5f27d6d009e26c4c8df64982b2461ade",
+ "renderedSha256": "b158508af21086c3a7c5ea40be3a151d5f27d6d009e26c4c8df64982b2461ade"
+ },
+ "agent-templates/sync/launch_session.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/launch_session.py",
+ "sourceSha256": "7fbe2aa20ec327b028d864469354dd697df7ce859999499955f2e95bede886da",
+ "renderedSha256": "7fbe2aa20ec327b028d864469354dd697df7ce859999499955f2e95bede886da"
+ },
+ "agent-templates/sync/requirements.txt": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/requirements.txt",
+ "sourceSha256": "462738ad889760ba38f2dc00976008a3c87201e17c9bc69972e5c213b6751d97",
+ "renderedSha256": "462738ad889760ba38f2dc00976008a3c87201e17c9bc69972e5c213b6751d97"
+ },
+ "agent-templates/sync/role_loader.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/role_loader.py",
+ "sourceSha256": "ac911e97bd594c29ccf6964fbf3bf218b286e79c0e6ae04b50367b1d0b326de6",
+ "renderedSha256": "ac911e97bd594c29ccf6964fbf3bf218b286e79c0e6ae04b50367b1d0b326de6"
+ },
+ "agent-templates/sync/sync_agents.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/sync_agents.py",
+ "sourceSha256": "6faafb497e8f8fbde6def1575069c6809775c314ed1e445b34adc83dd5a60e7c",
+ "renderedSha256": "6faafb497e8f8fbde6def1575069c6809775c314ed1e445b34adc83dd5a60e7c"
+ },
+ "agent-templates/sync/sync_environments.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/sync_environments.py",
+ "sourceSha256": "3aac67b84d61439b5203168138bd370d726b941e554f3c2fe284635663088e57",
+ "renderedSha256": "3aac67b84d61439b5203168138bd370d726b941e554f3c2fe284635663088e57"
+ },
+ "agent-templates/sync/sync_memory.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/sync_memory.py",
+ "sourceSha256": "6b204e3c95a6309e380a60d6b50ca486e06ab7f02278904a0bae65dcd0e42318",
+ "renderedSha256": "6b204e3c95a6309e380a60d6b50ca486e06ab7f02278904a0bae65dcd0e42318"
+ },
+ "agent-templates/sync/sync_vaults.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/sync_vaults.py",
+ "sourceSha256": "aa286b35e59f28361c953c5e6bbc8d2de6c97ece66d2605a027f46f4a05aee24",
+ "renderedSha256": "aa286b35e59f28361c953c5e6bbc8d2de6c97ece66d2605a027f46f4a05aee24"
+ },
+ "agent-templates/sync/validate.py": {
+ "capability": "roles",
+ "mode": "managed",
+ "sourcePath": "agent-templates/sync/validate.py",
+ "sourceSha256": "74c9fac452b18d218f5fa8a55e189b20d653c065d376c70f12ae1b5fd800b51f",
+ "renderedSha256": "74c9fac452b18d218f5fa8a55e189b20d653c065d376c70f12ae1b5fd800b51f"
+ },
+ "containers/templates/dev-typescript/.npmrc": {
+ "capability": "npm-registry",
+ "mode": "seeded",
+ "sourcePath": "community-templates/npmrc",
+ "sourceSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc",
+ "renderedSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc"
+ },
+ "containers/templates/dev-typescript/package.json": {
+ "capability": "node-toolchain",
+ "mode": "derived",
+ "sourcePath": "",
+ "sourceSha256": "43848df61bc70c6592224599c9eb5c722ce533f4c3516d7f4bd2d8e9edb8722f",
+ "renderedSha256": "43848df61bc70c6592224599c9eb5c722ce533f4c3516d7f4bd2d8e9edb8722f"
+ },
+ "e2e/.npmrc": {
+ "capability": "npm-registry",
+ "mode": "seeded",
+ "sourcePath": "community-templates/npmrc",
+ "sourceSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc",
+ "renderedSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc"
+ },
+ "e2e/package.json": {
+ "capability": "node-toolchain",
+ "mode": "derived",
+ "sourcePath": "",
+ "sourceSha256": "9a55034388abc2d4f8970a7c51aee2556eb1fb3f3247984e51ab4e11a859b3cd",
+ "renderedSha256": "9a55034388abc2d4f8970a7c51aee2556eb1fb3f3247984e51ab4e11a859b3cd"
+ },
+ "scripts/__tests__/test_gate_identifier.py": {
+ "capability": "entity-identity",
+ "mode": "managed",
+ "sourcePath": "scripts/__tests__/test_gate_identifier.py",
+ "sourceSha256": "d6b01fc736d59e4d1f1f8591f16f03cd4d1faa3a4b7bb422a155c13eb0040723",
+ "renderedSha256": "d6b01fc736d59e4d1f1f8591f16f03cd4d1faa3a4b7bb422a155c13eb0040723"
+ },
+ "scripts/build-frames-site.mjs": {
+ "capability": "workflows",
+ "mode": "managed",
+ "sourcePath": "scripts/build-frames-site.mjs",
+ "sourceSha256": "4fcb6bc3cb2d9c658622e55ada484ed9958a9591af36959301c6310a24d75aae",
+ "renderedSha256": "4fcb6bc3cb2d9c658622e55ada484ed9958a9591af36959301c6310a24d75aae"
+ },
+ "scripts/check-registration.mjs": {
+ "capability": "portal-registration",
+ "mode": "managed",
+ "sourcePath": "scripts/check-registration.mjs",
+ "sourceSha256": "82debe9d9c9ddfb24854a4a30a5d4602f13efa147209f5daa9698d187a922e0b",
+ "renderedSha256": "82debe9d9c9ddfb24854a4a30a5d4602f13efa147209f5daa9698d187a922e0b"
+ },
+ "scripts/gate_ds_conformance.py": {
+ "capability": "design-system",
+ "mode": "managed",
+ "sourcePath": "scripts/gate_ds_conformance.py",
+ "sourceSha256": "2155d4d75c739c1cf20ef03f3a364e66d8d4d0e47d9aa6844014caa82bfc1844",
+ "renderedSha256": "2155d4d75c739c1cf20ef03f3a364e66d8d4d0e47d9aa6844014caa82bfc1844"
+ },
+ "scripts/gate_identifier.py": {
+ "capability": "entity-identity",
+ "mode": "managed",
+ "sourcePath": "scripts/gate_identifier.py",
+ "sourceSha256": "ca1f8bfc2701346e949f1aa669fc4ace9a9586927379870e8dd67effe5d58ac8",
+ "renderedSha256": "ca1f8bfc2701346e949f1aa669fc4ace9a9586927379870e8dd67effe5d58ac8"
+ },
+ "scripts/gate_manifest.py": {
+ "capability": "schema-validate",
+ "mode": "managed",
+ "sourcePath": "scripts/gate_manifest.py",
+ "sourceSha256": "527abd1ed32ff9a928b65e082b306b4055e55d1346cf031a3b8b6ec70ccd76b1",
+ "renderedSha256": "527abd1ed32ff9a928b65e082b306b4055e55d1346cf031a3b8b6ec70ccd76b1"
+ },
+ "scripts/gate_pagination.py": {
+ "capability": "workflows",
+ "mode": "managed",
+ "sourcePath": "scripts/gate_pagination.py",
+ "sourceSha256": "7f1e0d79e2a80a79250db72ff8e437c9cb4a7b596728c4c9b377d66c080f6c35",
+ "renderedSha256": "7f1e0d79e2a80a79250db72ff8e437c9cb4a7b596728c4c9b377d66c080f6c35"
+ },
+ "scripts/stamp-frames.mjs": {
+ "capability": "workflows",
+ "mode": "managed",
+ "sourcePath": "scripts/stamp-frames.mjs",
+ "sourceSha256": "71ede05b3cf69848c595302b4d80a041d2e7cb177e7afa8b2fe68ace2d04d6b9",
+ "renderedSha256": "71ede05b3cf69848c595302b4d80a041d2e7cb177e7afa8b2fe68ace2d04d6b9"
+ },
+ "scripts/sync-chart-files.sh": {
+ "capability": "portal-registration",
+ "mode": "managed",
+ "sourcePath": "scripts/sync-chart-files.sh",
+ "sourceSha256": "1d6a570f53a8ad1b0fd76b383fe4d54329a6f6568a150d8d0a07d7d2c0bd8534",
+ "renderedSha256": "1d6a570f53a8ad1b0fd76b383fe4d54329a6f6568a150d8d0a07d7d2c0bd8534"
+ },
+ "services/ui-react/.npmrc": {
+ "capability": "npm-registry",
+ "mode": "seeded",
+ "sourcePath": "community-templates/npmrc",
+ "sourceSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc",
+ "renderedSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc"
+ },
+ "services/ui-react/package.json": {
+ "capability": "node-toolchain",
+ "mode": "derived",
+ "sourcePath": "",
+ "sourceSha256": "a01d20bf1acfe700177632cc3209d285b6d7036300efa9b184319dda9c59e3ee",
+ "renderedSha256": "a01d20bf1acfe700177632cc3209d285b6d7036300efa9b184319dda9c59e3ee"
+ },
+ "services/ui/.npmrc": {
+ "capability": "npm-registry",
+ "mode": "seeded",
+ "sourcePath": "community-templates/npmrc",
+ "sourceSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc",
+ "renderedSha256": "0dc82809c288fc8380df0d3701f3abe8bbb594860f7425ab97a0f82bd69469fc"
+ },
+ "services/ui/package.json": {
+ "capability": "node-toolchain",
+ "mode": "derived",
+ "sourcePath": "",
+ "sourceSha256": "0270553a7ef61ad50109fc6961c852a78aeecd6c95660020f6240ab4b5b5d518",
+ "renderedSha256": "0270553a7ef61ad50109fc6961c852a78aeecd6c95660020f6240ab4b5b5d518"
+ }
+ }
+}
diff --git a/.fuze/repo-manifest.schema.json b/.fuze/repo-manifest.schema.json
new file mode 100644
index 0000000..122983d
--- /dev/null
+++ b/.fuze/repo-manifest.schema.json
@@ -0,0 +1,301 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://github.com/izzywdev/FuzeSDLC/governance/repo-manifest.schema.json",
+ "title": "Fuze repo manifest",
+ "description": "Declares which canonical FuzeSDLC agents/skills a repo instantiates and its hardening settings. Lives at .fuze/manifest.json in each consuming repo; sdlc-bootstrap reads it.",
+ "type": "object",
+ "required": ["repo", "class", "tier", "expert", "agents"],
+ "additionalProperties": false,
+ "properties": {
+ "repo": { "type": "string", "description": "owner/name, e.g. izzywdev/FuzeFront" },
+ "class": { "enum": ["oss-public", "commercial-private"], "description": "Licensing/contribution posture (see governance/repo-classes.md). Must match visibility: public=oss-public (MIT, open), private=commercial-private (proprietary LICENSE+NOTICE, internal-only). A commercial-private repo must NOT ship a permissive OSS license." },
+ "tier": { "enum": ["governance", "infra", "product", "library"], "description": "Position in the repo-tier model." },
+ "expert": { "type": "string", "description": "The required -expert agent name, e.g. fuzefront-expert." },
+ "baselineRef": { "type": "string", "default": "v1", "description": "FuzeSDLC ref this repo pins its baseline/agents/skills to; governance-sync reconciles against it. Use the moving major tag 'v1' — re-pointed on every compatible policy update, so propagation stays automatic while breaking changes land as 'v2' and are opted into per repo. Pin an immutable 'v1.x.y' only to deliberately freeze a repo. Do NOT pin 'main': unreviewed policy would propagate instantly. See governance/versioning.md." },
+ "agents": {
+ "type": "array",
+ "description": "Canonical agent names this repo instantiates (subset of FuzeSDLC/agents). Conditional channel agents only when the repo ships that channel.",
+ "items": { "type": "string" },
+ "minItems": 1
+ },
+ "roles": {
+ "type": "object",
+ "description": "OPTIONAL Managed-Agents role definitions this repo provisions (agent-templates/). The FRAMEWORK (schema/roles/_base/sync/providers) is canonical in FuzeSDLC — stamped by sdlc-bootstrap, reconciled by governance-sync; the concrete roles/environments/vaults/coordinators are the repo's own. Present -> the repo gets the provision-sync.yml caller. See skills/managed-agents-roles.",
+ "additionalProperties": false,
+ "properties": {
+ "source": { "type": "string", "default": "agent-templates/", "description": "Path to the agent-templates tree in this repo." },
+ "runtime": { "enum": ["managed-agents"], "default": "managed-agents", "description": "Agent runtime the roles target." },
+ "provider": { "enum": ["anthropic", "openai", "hermes"], "default": "anthropic", "description": "Provider the roles are provisioned to (providers//adapter.py)." },
+ "defined": {
+ "type": "array",
+ "description": "Role keys defined under roles// (excluding _base), e.g. backend, frontend, devops.",
+ "items": { "type": "string" }
+ },
+ "environments": {
+ "type": "array",
+ "description": "Environment names defined under environments/, e.g. cloud-backend, selfhosted-devops.",
+ "items": { "type": "string" }
+ },
+ "coordinators": {
+ "type": "array",
+ "description": "Coordinator role keys under coordinator/, e.g. coordinator, exec-coordinator.",
+ "items": { "type": "string" }
+ }
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "DEPRECATED — declare the typed block instead (`mcp`, `mobile`, …). This array names a channel without configuring it, and the two have drifted completely apart: 13 repos carry an `mcp` block with no `mcp` channel, and not one of the 10 repos with a `mobile` block declares `mobile-app`. Only FuzeAgent and FuzePlan agree with themselves. The typed block is authoritative for capability installation. Deliberately NOT linked to the blocks by `dependentRequired` — any such constraint fails 19 of 22 manifests today, which would make the schema unadoptable rather than make the fleet correct.",
+ "items": { "enum": ["mobile-app", "desktop-app", "cli", "mcp", "wordpress"] }
+ },
+ "toolchain": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "Node/React floor for this repo. MANAGED — governance_sync reconciles every key except `enforce` from governance/toolchain.json, so raising the family floor is one edit in the canonical rather than 20 PRs. `moduleFederationRequiredVersion` is the one with a runtime consequence: it must be byte-identical in the MF host and every remote, or the remote loads its own React copy and dies on 'Invalid hook call' in the browser with nothing in CI to catch it.",
+ "properties": {
+ "node": { "type": "string", "description": "engines.node floor, e.g. '>=24.0.0'." },
+ "npm": { "type": "string", "description": "engines.npm floor, e.g. '>=10.0.0'." },
+ "typesNode": { "type": "string", "description": "@types/node range." },
+ "react": { "type": "string", "description": "react / react-dom app dependency range." },
+ "reactPeerRange": { "type": "string", "description": "peerDependencies React range for published packages." },
+ "typesReact": { "type": "string", "description": "@types/react and @types/react-dom range." },
+ "moduleFederationRequiredVersion": { "type": "string", "description": "MF shared requiredVersion. Must match the host EXACTLY." },
+ "dockerBaseImage": { "type": "string", "description": "Mandated Docker base image, e.g. 'node:24-alpine'." },
+ "ciNodeVersion": { "type": "string", "description": "setup-node node-version, e.g. '24.x'." },
+ "enforce": { "type": "boolean", "default": false, "description": "REPO-OWNED, never reconciled. false = gate-toolchain warns; true = it fails. Each repo flips this in the same PR that lands its bumps, so the gate is real from the first commit at which the repo can pass it. governance-nightly reports anyone still on false, so it cannot quietly become permanent." },
+ "note": { "type": "string" }
+ }
+ },
+ "identity": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "Entity-identifier standard (governance/identifier-standard.md). Declaring this block is what makes gate-identifier's --adoption check ENFORCING for the repo, so it is an opt-in with teeth rather than a label.",
+ "properties": {
+ "namespace": { "type": "string", "pattern": "^[a-z][a-z0-9]*$", "description": "Prefix namespace for this product's entity types. DECLARED, never derived from the repo directory name — a rename would otherwise silently orphan every id already issued, surfacing much later as unresolvable references with no obvious cause. Only FuzeFront may mint the bare reserved spine prefixes (usr/org/prt/app, billing, messaging); a product's own types namespace as _." },
+ "packages": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "The identity package this repo depends on. gate-identifier --adoption fails a repo that mints entity ids without one.",
+ "properties": {
+ "node": { "type": "string", "description": "e.g. '@izzywdev/fuzefront-identity'." },
+ "python": { "type": "string", "description": "e.g. 'fuzefront-identity' — installed from a tagged GitHub Release wheel, because GitHub Packages has no PyPI registry." }
+ }
+ },
+ "note": { "type": "string" }
+ }
+ },
+ "portal": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "Whether this repo registers itself as an app in the FuzeFront portal.",
+ "properties": {
+ "registers": { "type": "boolean", "description": "false opts the repo out of portal registration entirely; bootstrap skips the portal-registration capability." },
+ "reason": { "type": "string", "description": "Why, when registers is false." }
+ }
+ },
+ "platformAuth": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "NEW BLOCK — no repo declares it yet, and that is the point: it gates the platform-auth capability. Consume @fuzefront/auth (published as @izzywdev/fuzefront-auth) rather than a bespoke verifier. A product NEVER calls Permit directly; it knows exactly one thing, the base URL of FuzeFront's Security API.",
+ "properties": {
+ "mode": { "enum": ["federated-jwks", "legacy-hs256", "session-delegation"], "description": "federated-jwks verifies Authentik-issued tokens against a cached JWKS and is the target. legacy-hs256 is a symmetric shared secret — a minting capability, interim only. session-delegation calls GET /api/v1/security/session instead of verifying locally." },
+ "package": { "type": "string", "description": "e.g. '@izzywdev/fuzefront-auth'." },
+ "authzBaseUrl": { "type": "string", "description": "Base URL of the FuzeFront Security API for createAuthzClient. Same-origin in the portal; never an absolute API host baked into a frontend." },
+ "note": { "type": "string" }
+ }
+ },
+ "a2a": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "Agent-to-agent surface. Mirrors the frozen contract at FuzeAgent/agent-templates/contracts/a2a/v1/schema/manifest-a2a-extension.schema.json — mirrored rather than $ref'd because this file carries an absolute $id, which hijacks relative reference resolution and turns the ref into a GitHub HTML URL no validator will fetch. Note the AUTHORITATIVE caller allowlist is the top-level `providesTo`, not anything here: absent is NOT the same as empty, and absent must fail closed.",
+ "properties": {
+ "enabled": { "type": "boolean", "default": false, "description": "Whether this repo publishes an A2A agent surface at all. Mirrors the Helm enabled gate." },
+ "servingRoles": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9_-]+$" }, "description": "Role keys projected into card skills. Order is preserved and becomes skill order." },
+ "entryRole": { "type": "string", "pattern": "^[a-z0-9_-]+$", "description": "Role that receives an incoming SendMessage when the caller names no skill. Optional — FuzeInfra declares servingRoles without it, so this must not be required." },
+ "external": { "type": "boolean", "default": false, "description": "true also publishes the card through the Cloudflare tunnel with Access in front. Default false = in-cluster service DNS only." },
+ "documentationUrl": { "type": "string", "format": "uri" },
+ "iconUrl": { "type": "string", "format": "uri" },
+ "note": { "type": "string" },
+ "_note": { "type": "string", "description": "Legacy spelling of `note`, used by FuzeKeys. Accepted so the schema does not reject real manifests — a bare $ref to the frozen contract rejects 6 of 16 over exactly this. Prefer `note` in new work." }
+ }
+ },
+ "mcp": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "Model Context Protocol surface. The fleet standardised on the SHARED OpenAPI->MCP gateway image rather than per-repo server code, so `servers[]` has two legitimate and mutually-exclusive dialects — see its description. Do not add a `required` that assumes one.",
+ "properties": {
+ "enabled": { "type": "boolean", "default": false },
+ "entryServer": { "type": "string", "description": "Which entry in servers[] is the default target." },
+ "transport": { "type": "string", "description": "Repo-level default transport. Legitimately also appears per-server; the per-server value wins." },
+ "protocolVersion": { "type": "string", "description": "MCP spec date, e.g. '2025-06-18'." },
+ "toolCount": { "type": "integer", "description": "Expected tool count, verified by booting the gateway against the repo's spec." },
+ "irreversibleTools": { "type": "array", "items": { "type": "string" }, "description": "Tools that mutate irreversibly. Each MUST resolve in the generated tools.json with mutates:true — the gateway refuses to start on a contradictory override, and that refusal IS the test." },
+ "note": { "type": "string" },
+ "servers": {
+ "type": "array",
+ "description": "TWO DIALECTS, both valid. (a) stdio/entry: {name, transport, entry} where entry points at server code. (b) gateway: {name, transport, image, implementation, spec, overrides, manifest, deployment, endpoints} with NO entry, because the shared gateway needs no per-repo server code. A third hybrid sets kind:'openapi-gateway' with entry pointing at the spec. Only `name` and `transport` are universal — requiring `entry` would reject FuzeSales and FuzeService.",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "transport"],
+ "properties": {
+ "name": { "type": "string" },
+ "transport": { "type": "string", "description": "e.g. 'stdio' or 'sse'." },
+ "kind": { "type": "string", "description": "e.g. 'openapi-gateway'." },
+ "entry": { "type": "string", "description": "Path to server code, or to the spec in the hybrid dialect." },
+ "image": { "type": "string", "description": "Shared gateway image, e.g. 'ghcr.io/izzywdev/fuze-mcp-gateway'." },
+ "implementation": { "type": "string" },
+ "spec": { "type": "string", "description": "OpenAPI contract the gateway projects into tools." },
+ "overrides": { "type": "string", "description": "e.g. 'mcp/tools.overrides.yaml'." },
+ "manifest": { "type": "string", "description": "Generated tool manifest, e.g. 'mcp/tools.json'." },
+ "toolManifest": { "type": "string", "description": "Alternate spelling of `manifest`, used by FuzeX." },
+ "deployment": { "type": "string", "description": "Chart template that deploys the gateway." },
+ "endpoints": {
+ "type": "object",
+ "description": "Served paths, e.g. {sse, messages, health, tools}.",
+ "additionalProperties": { "type": "string" }
+ }
+ }
+ }
+ }
+ }
+ },
+ "mobile": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["required"],
+ "description": "Mobile requirements. Mirrors agent-templates/schema/mobile-requirements.schema.json, whose own description already sanctions living as a `mobile` block inside .fuze/manifest.json. Mirrored rather than $ref'd: that file's $id omits its own `schema/` path segment so it is not findable by $id, and this file's absolute $id breaks relative resolution. All 10 real blocks validate against the mirrored shape.",
+ "properties": {
+ "$schema": { "type": "string" },
+ "product": { "type": "string" },
+ "required": { "type": "boolean", "description": "Whether mobile support is mandatory for this product." },
+ "strategy": { "enum": ["responsive-web", "pwa", "react-native", "flutter"] },
+ "targets": { "type": "array", "items": { "enum": ["ios", "android", "mobile-web"] } },
+ "responsive": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min_width": { "type": "integer", "description": "Narrowest supported viewport; no horizontal scroll at this width." },
+ "breakpoints": { "type": "array", "items": { "type": "integer" } },
+ "min_tap_target_px": { "type": "integer", "default": 44 }
+ }
+ },
+ "acceptance": {
+ "type": "array",
+ "description": "Prose criteria, or a scored check. States are contract: cover loading/empty/error, not just the happy path.",
+ "items": {
+ "oneOf": [
+ { "type": "string" },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["check"],
+ "properties": {
+ "check": { "type": "string" },
+ "min_score": { "type": "number" },
+ "at_width": { "type": "integer" }
+ }
+ }
+ ]
+ }
+ },
+ "packages": { "type": "array", "items": { "type": "string" } },
+ "owner": { "type": "string" }
+ }
+ },
+ "designSystem": {
+ "type": "object",
+ "description": "Frontend repos only.",
+ "properties": {
+ "base": { "type": "string", "default": "@fuzefront/design-system", "description": "The inherited base DS package." },
+ "extendsAs": { "type": "string", "description": "This repo's local DS package that extends the base, e.g. @fuzex/design-system." }
+ }
+ },
+ "hardening": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ruleset": { "type": "boolean", "default": true },
+ "requireSignatures": { "type": "boolean", "default": true },
+ "requiredChecks": {
+ "type": "array",
+ "description": "Extra repo-specific required status-check contexts beyond the six gate-* (e.g. FuzeInfra: test-infrastructure).",
+ "items": { "type": "string" }
+ },
+ "deployOnPush": { "type": "boolean", "default": false, "description": "If true, never bot-merge; merge in a deploy window." }
+ }
+ },
+ "modelCascade": {
+ "type": "object",
+ "description": "OPTIONAL per-repo overrides for the model cascade (governance/model-cascade.md). Absent → canonical defaults from the copied agent files apply.",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": { "type": "boolean", "default": true, "description": "Set false to opt this repo out of tiered cascade execution (run single-tier)." },
+ "tierOverrides": {
+ "type": "object",
+ "description": "Per-agent default-tier overrides, e.g. { \"backend-engineer\": \"opus\" }. Keys are canonical agent names; values are the tier to run that agent at in this repo.",
+ "additionalProperties": { "enum": ["opus", "sonnet", "haiku", "fable"] }
+ }
+ }
+ },
+ "platformServices": {
+ "type": "object",
+ "description": "Which shared platform-spine services this product consumes + the integration point for each (governance/platform-services.md). Every product consumes all that apply.",
+ "additionalProperties": false,
+ "properties": {
+ "deploy": { "type": "string", "description": "FuzeDeploy CI/CD integration." },
+ "front": { "type": "string", "description": "FuzeFront UX/portal: module-federation remote + @fuzefront/design-system." },
+ "infra": { "type": "string", "description": "FuzeInfra: vendored submodule + consumer-test + Helm/Argo." },
+ "plan": { "type": "string", "description": "FuzePlan (Both): @fuzefront/fuzeplan-client + tracked project." },
+ "agent": { "type": "string", "description": "FuzeAgent: agent dispatch/launch via the FuzeAgent orchestrator." },
+ "keys": { "type": "string", "description": "FuzeKeys: identity + PII tokenize/detokenize client." },
+ "picker": { "type": "string", "description": "FuzePicker: real-time in-browser user-comms widget." }
+ }
+ },
+ "dataTier": {
+ "type": "array",
+ "description": "Declarative data-tier provisioning request (the IaC hand-off to FuzeInfra). FuzeInfra's reconciler consumes each entry: it ensures the per-service role exists AND is GRANTED the declared privileges on the declared database, then VERIFIES the role can actually read/write it (fail-loud if a role can auth but not access its DB). Replaces the old ad-hoc '@claude please provision' request (governance/shared-cluster-deploy.md §5). Every store the product's role authenticates to MUST be declared here, with the exact database name the app uses — a role granted on the wrong db name is the classic silent-empty-data bug.",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["store", "database", "role"],
+ "properties": {
+ "store": { "enum": ["postgres", "mongo", "redis", "neo4j", "chroma"], "description": "Shared datastore this role needs access in." },
+ "database": { "type": "string", "description": "The exact database/keyspace name the app reads/writes (e.g. robot_catalog). The role MUST be granted on THIS name; provisioning verifies it." },
+ "role": { "type": "string", "description": "The per-service role/user (e.g. mendys)." },
+ "privileges": { "enum": ["readWrite", "read", "admin"], "default": "readWrite", "description": "Privilege level to grant the role on `database`." },
+ "authSource": { "type": "string", "description": "Mongo authSource db the role authenticates against (e.g. admin), when it differs from `database`." }
+ }
+ }
+ },
+ "egress": {
+ "type": "array",
+ "description": "External hosts the product's pods need outbound HTTPS to. The shared cluster is egress-restricted (HTTP-only behind the Cloudflare tunnel; no default outbound to third-party APIs), so every external dependency MUST be declared here. FuzeInfra's reconciler turns these into namespace egress allow-rules (NetworkPolicy / egress gateway). Declare each third-party API explicitly (e.g. LLM providers).",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["host"],
+ "properties": {
+ "host": { "type": "string", "description": "FQDN, e.g. api.openai.com." },
+ "port": { "type": "integer", "default": 443, "description": "Destination port (default 443)." },
+ "reason": { "type": "string", "description": "Why the product needs it (e.g. 'AI keyword generation')." }
+ }
+ }
+ },
+ "dependsOn": {
+ "type": "array",
+ "description": "Product-to-product dependencies this repo consumes beyond the spine (e.g. FuzeService dependsOn FuzeContact, FuzeBI).",
+ "items": { "type": "string" }
+ },
+ "providesTo": {
+ "type": "array",
+ "description": "Products that consume this one (e.g. FuzeContact providesTo FuzeService, FuzeSales).",
+ "items": { "type": "string" }
+ },
+ "expertMonitors": {
+ "type": "array",
+ "description": "Files and directories the repo's expert agent monitors for drift. governance-nightly reads these paths and compares their content against the expert's claims; if significant drift is found it opens an auto-refresh PR. Declare paths that are 'architecture-defining' for this repo: Helm charts, CI workflows, the service's main OpenAPI spec, key source directories. Example: [\"deploy/helm/\", \".github/workflows/\", \"openapi.yaml\", \"src/services/\"].",
+ "items": { "type": "string" }
+ }
+ }
+}
diff --git a/.github/labels.json b/.github/labels.json
new file mode 100644
index 0000000..c4e0f2e
--- /dev/null
+++ b/.github/labels.json
@@ -0,0 +1,13 @@
+[
+ { "name": "auto-merge", "color": "0052cc", "description": "Enable squash auto-merge once CI passes" },
+ { "name": "wip", "color": "ededed", "description": "Work in progress; exempt from staleness/auto-merge" },
+ { "name": "hold", "color": "ededed", "description": "On hold; exempt from staleness/auto-merge" },
+ { "name": "blocked", "color": "b60205", "description": "Blocked on an external dependency; exempt from staleness" },
+ { "name": "argo-autofix", "color": "b60205", "description": "ArgoCD self-heal / deploy autofix" },
+ { "name": "integration", "color": "1d76db", "description": "Nightly integration tests" },
+ { "name": "security", "color": "b60205", "description": "Security finding / hardening" },
+ { "name": "design-review", "color": "5319e7", "description": "Awaiting product-owner visual design approval" },
+ { "name": "design-approval", "color": "7b6cf6", "description": "Design-frame approval decision (approve/reject a flow)" },
+ { "name": "ds-extraction", "color": "5319e7", "description": "Candidate UI pattern to extract into the design system" },
+ { "name": "ds-propagation", "color": "0075ca", "description": "Design system propagation task" }
+]
diff --git a/.github/workflows/a2a-maintain.yml b/.github/workflows/a2a-maintain.yml
index 24017b5..8f3d5d0 100644
--- a/.github/workflows/a2a-maintain.yml
+++ b/.github/workflows/a2a-maintain.yml
@@ -46,7 +46,7 @@ jobs:
- name: Checkout PR head (writable, same-repo only)
if: steps.guard.outputs.ok == 'true'
- uses: actions/checkout@v4
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
index 4783c94..b9ced5d 100644
--- a/.github/workflows/auto-merge.yml
+++ b/.github/workflows/auto-merge.yml
@@ -6,8 +6,10 @@ on:
check_suite:
types: [completed]
-# The default GITHUB_TOKEN is read-only, so `gh pr merge --auto` fails with
-# "Resource not accessible by integration".
+# The default GITHUB_TOKEN is read-only unless a permissions block grants more.
+# `gh pr merge --auto` needs pull-requests: write (to enable auto-merge) and
+# contents: write (for the squash + branch delete); without these the step
+# fails with "Resource not accessible by integration".
permissions:
contents: write
pull-requests: write
@@ -20,11 +22,10 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Enable auto-merge
if: github.event.pull_request.user.login == github.repository_owner || contains(github.event.pull_request.labels.*.name, 'auto-merge')
- continue-on-error: true
run: |
gh pr merge ${{ github.event.pull_request.number }} --auto --squash --delete-branch
env:
diff --git a/.github/workflows/claude-auto-pr.yml b/.github/workflows/claude-auto-pr.yml
index bf41bd3..a0d8625 100644
--- a/.github/workflows/claude-auto-pr.yml
+++ b/.github/workflows/claude-auto-pr.yml
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
diff --git a/.github/workflows/claude-ci-autofix.yml b/.github/workflows/claude-ci-autofix.yml
index 3dbda7d..5a0c82f 100644
--- a/.github/workflows/claude-ci-autofix.yml
+++ b/.github/workflows/claude-ci-autofix.yml
@@ -2,7 +2,7 @@ name: Claude Auto-Fix CI
on:
workflow_run:
- workflows: ["CI/CD Pipeline", "FuzeAgent Test Suite"]
+ workflows: ["Infrastructure Tests", "Helm Chart Validation", "Harden Gate"]
types: [completed]
permissions:
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index 4afa8ac..d7514a4 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -32,11 +32,40 @@ jobs:
actions: read
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
+ - name: Checkout PR branch (when triggered from a PR)
+ if: |
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) ||
+ github.event_name == 'pull_request_review_comment' ||
+ github.event_name == 'pull_request_review'
+ # Every `github` context value reaches the shell through env:, never through
+ # `${{ }}` inside run:. A branch name is attacker-controlled and git permits `;`,
+ # `|`, `$`, `"` and backticks in a ref, so an interpolated head.ref executes on the
+ # runner with the job's token in scope — the documented GitHub Actions script
+ # injection. actionlint flags it as `expression`, semgrep as run-shell-injection.
+ run: |
+ if [ "$EVENT_NAME" = "issue_comment" ]; then
+ HEAD_SHA=$(gh api "repos/${REPO}/pulls/${PR_NUM}" --jq .head.sha)
+ HEAD_REF=$(gh api "repos/${REPO}/pulls/${PR_NUM}" --jq .head.ref)
+ else
+ HEAD_SHA="$PR_HEAD_SHA"
+ HEAD_REF="$PR_HEAD_REF"
+ fi
+ git fetch origin "$HEAD_SHA"
+ git checkout "$HEAD_SHA"
+ echo "Checked out PR branch: $HEAD_REF @ $HEAD_SHA"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ EVENT_NAME: ${{ github.event_name }}
+ REPO: ${{ github.repository }}
+ PR_NUM: ${{ github.event.issue.number }}
+ PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
+
- name: Run Claude
- uses: anthropics/claude-code-action@v1
+ uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
diff --git a/.github/workflows/design-approval.yml b/.github/workflows/design-approval.yml
new file mode 100644
index 0000000..7790e4a
--- /dev/null
+++ b/.github/workflows/design-approval.yml
@@ -0,0 +1,298 @@
+name: design-approval
+
+# Turns an in-frame "Approve" click into a verified, signed manifest flip.
+#
+# The in-frame approval control opens a prefilled GitHub issue (label
+# `design-approval`) carrying a YAML block: feature / flow / route / decision / stamp.
+# This workflow enforces, IN ORDER and FAIL-CLOSED:
+# 1. Authorization — issue author must be the repo owner or a CODEOWNER
+# (derived from .github/CODEOWNERS, never hand-listed here).
+# 2. Staleness — recompute the feature's current stamp (scripts/stamp-frames.mjs)
+# and compare to the issue's stamp. Mismatch (or empty) → do NOT flip.
+# 3. Idempotency — if the flow is already approved at this stamp → "already recorded".
+# 4. Write — on approve, flip build.flows[].approved on the default
+# branch via a DEPLOY KEY (secrets.DESIGN_APPROVAL_DEPLOY_KEY).
+# This is deliberate: on a hardened default branch (required_signatures + a
+# pull_request rule), a Contents-API commit under GITHUB_TOKEN is REJECTED
+# ("Repository rule violations found"), and GitHub refuses to add the
+# github-actions app as a ruleset bypass actor on a user-owned repo. A push
+# deploy key registered as a DeployKey bypass_actor on the branch ruleset
+# clears the PR rule AND required_signatures. Register that key and store its
+# private half in the DESIGN_APPROVAL_DEPLOY_KEY secret — this is a design-first
+# hardening prerequisite (see governance/hardening-convention.md). NOTE this is
+# a DIFFERENT key from the read-only FUZESDLC_DEPLOY_KEY (which only pulls from
+# FuzeSDLC); this one PUSHES the approval flip to this repo's own default branch.
+# On reject, comment and leave the issue open for the designer to iterate.
+#
+# Approval bookkeeping (approved/approvedBy/approvedAt) is EXCLUDED from the stamp
+# (scripts/stamp-frames.mjs), so flipping a flow to approved does not change the
+# stamp — sibling flows keep the same valid stamp and per-flow approval works.
+#
+# on: issues:[opened] ONLY — edits can't re-fire (avoids double-trigger). Every
+# expectation is DERIVED from a source of truth (stamp from files, flip target from
+# the parsed issue, authorization from CODEOWNERS); nothing is hand-mirrored.
+
+on:
+ issues:
+ types: [opened]
+
+permissions:
+ contents: read # the flip is pushed via the deploy key, not GITHUB_TOKEN
+ issues: write # comment + close
+
+concurrency:
+ group: design-approval-${{ github.event.issue.number }}
+ cancel-in-progress: false
+
+jobs:
+ approve:
+ if: contains(github.event.issue.labels.*.name, 'design-approval')
+ runs-on: ubuntu-latest
+ steps:
+ # Actions pinned to immutable commit SHAs (repo hardening standard).
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ with:
+ # frames + the manifest we flip live on the default branch
+ ref: ${{ github.event.repository.default_branch }}
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '24.x'
+
+ - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ id: decide
+ with:
+ script: |
+ const fs = require('fs');
+ const path = require('path');
+ const { execFileSync } = require('child_process');
+ const { owner, repo } = context.repo;
+ const issue = context.payload.issue;
+ const issueNumber = issue.number;
+ const author = issue.user.login;
+
+ const comment = (body) =>
+ github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body });
+
+ // ---- Parse the YAML block (fail-closed). ----
+ function parseBlock(body) {
+ if (!body) throw new Error('issue body is empty');
+ // Prefer a fenced ```yaml block; else scan the whole body for key: value.
+ const fence = body.match(/```(?:ya?ml)?\s*([\s\S]*?)```/i);
+ const text = fence ? fence[1] : body;
+ const out = {};
+ for (const rawLine of text.split(/\r?\n/)) {
+ const m = rawLine.match(/^\s*([A-Za-z_][\w-]*)\s*:\s*(.*?)\s*$/);
+ if (!m) continue;
+ let v = m[2].replace(/^["']|["']$/g, '').trim();
+ out[m[1].toLowerCase()] = v;
+ }
+ return out;
+ }
+
+ let data;
+ try {
+ data = parseBlock(issue.body);
+ } catch (e) {
+ await comment(`Design approval could not parse this issue. No action taken (fail-closed).\n\n> ${e.message}`);
+ core.setFailed(`parse error: ${e.message}`);
+ return;
+ }
+
+ const feature = data.feature;
+ const flowId = data.flow;
+ const decision = (data.decision || '').toLowerCase();
+ const issueStamp = data.stamp || '';
+ if (!feature || !flowId || !decision) {
+ await comment(`Design approval block is missing required fields (need feature, flow, decision). No action taken (fail-closed).\n\nParsed: \`${JSON.stringify(data)}\``);
+ core.setFailed('missing required fields');
+ return;
+ }
+
+ // ---- 1. Authorization — owner or a CODEOWNER (derived, not restated). ----
+ function codeownerLogins() {
+ const logins = new Set([owner.toLowerCase()]);
+ for (const p of ['.github/CODEOWNERS', 'CODEOWNERS', 'docs/CODEOWNERS']) {
+ if (fs.existsSync(p)) {
+ for (const mm of fs.readFileSync(p, 'utf8').matchAll(/@([A-Za-z0-9-]+)/g)) {
+ logins.add(mm[1].toLowerCase());
+ }
+ }
+ }
+ return logins;
+ }
+ const authorized = codeownerLogins().has(author.toLowerCase());
+ if (!authorized) {
+ await comment(`@${author} is not authorized to approve designs (must be the repo owner or a CODEOWNER). No action taken.`);
+ core.setFailed(`unauthorized actor: ${author}`);
+ return;
+ }
+
+ // ---- Locate the manifest + flow (flip target derived from the issue). ----
+ const manifestPath = path.join('design', 'frames', feature, 'manifest.json');
+ if (!fs.existsSync(manifestPath)) {
+ await comment(`No manifest found for feature \`${feature}\` at \`${manifestPath}\`. No action taken (fail-closed).`);
+ core.setFailed(`manifest not found: ${manifestPath}`);
+ return;
+ }
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
+ const flows = manifest?.build?.flows;
+ if (!Array.isArray(flows)) {
+ await comment(`Manifest for \`${feature}\` has no \`build.flows\` array. No action taken (fail-closed).`);
+ core.setFailed('no build.flows');
+ return;
+ }
+ const flow = flows.find((f) => f.id === flowId);
+ if (!flow) {
+ const ids = flows.map((f) => f.id).join(', ');
+ await comment(`Flow \`${flowId}\` not found in \`${feature}\` (available: ${ids}). No action taken (fail-closed).`);
+ core.setFailed(`flow not found: ${flowId}`);
+ return;
+ }
+
+ // ---- 2. Staleness — recompute current stamp from the files. ----
+ let computedStamp;
+ try {
+ const out = execFileSync('node', ['scripts/stamp-frames.mjs', '--feature', feature, '--json'], { encoding: 'utf8' });
+ computedStamp = JSON.parse(out.slice(out.indexOf('{')))[feature];
+ } catch (e) {
+ await comment(`Could not compute the current stamp for \`${feature}\`. No action taken (fail-closed).\n\n> ${e.message}`);
+ core.setFailed(`stamp compute failed: ${e.message}`);
+ return;
+ }
+ if (!computedStamp) {
+ await comment(`No stamp could be computed for \`${feature}\`. No action taken (fail-closed).`);
+ core.setFailed('no computed stamp');
+ return;
+ }
+ if (!issueStamp) {
+ await comment(`This approval carries no \`stamp\`, so staleness cannot be verified against the frames you saw. A maintainer must confirm and re-stamp before this can be recorded. No flip performed (fail-closed).\n\nCurrent computed stamp: \`${computedStamp}\``);
+ core.setFailed('empty issue stamp — cannot verify');
+ return;
+ }
+ if (issueStamp !== computedStamp) {
+ await comment(`The frames changed since you viewed them (stamp \`${issueStamp}\` ≠ \`${computedStamp}\`). This approval will NOT be recorded — please re-review the current frames and approve again. No flip performed (fail-closed).`);
+ core.setFailed(`stale stamp: issue=${issueStamp} computed=${computedStamp}`);
+ return;
+ }
+
+ // ---- 3. Idempotency — already approved at this stamp? ----
+ if (flow.approved === true && manifest.stamp === computedStamp) {
+ await comment(`Flow \`${flowId}\` of \`${feature}\` is already recorded as approved at this stamp (\`${computedStamp}\`). A double-submit is not a second approval — no action taken.`);
+ if (issue.state === 'open') {
+ await github.rest.issues.update({ owner, repo, issue_number: issueNumber, state: 'closed' });
+ }
+ return;
+ }
+
+ // ---- Reject → comment onto the record, leave open, do NOT flip. ----
+ if (decision !== 'approve') {
+ const notes = data.notes ? `\n\n> ${data.notes}` : '';
+ await comment(`Decision recorded: **${decision}** for flow \`${flowId}\` of \`${feature}\` by @${author}. The frames are NOT flipped; the issue stays open for the designer to iterate.${notes}`);
+ return;
+ }
+
+ // ---- 4. Write — hand the flip to the deploy-key push step. ----
+ // The actual default-branch write CANNOT go through the Contents API here
+ // (a hardened branch ruleset rejects github-actions commits — see header).
+ // We only decide + authorize; the next step performs a race-safe flip
+ // against the fresh default branch over the DeployKey bypass and pushes. The final
+ // comment + close then run only after that push actually succeeds.
+ core.setOutput('flip', 'true');
+ core.setOutput('feature', feature);
+ core.setOutput('flow', flowId);
+ core.setOutput('author', author);
+ core.setOutput('stamp', computedStamp);
+ core.setOutput('manifest_path', manifestPath);
+
+ # The default-branch write path — a DEPLOY KEY. On a hardened branch a
+ # Contents-API/GITHUB_TOKEN commit is rejected with "Repository rule
+ # violations found" (PR rule + required_signatures), and the github-actions
+ # app cannot be a ruleset bypass actor on a user-owned repo. The deploy key
+ # in DESIGN_APPROVAL_DEPLOY_KEY must be registered as a DeployKey bypass_actor
+ # on the branch ruleset, so its push clears both the PR rule and
+ # required_signatures. If this starts failing with a rule violation, the
+ # DeployKey bypass entry or the deploy key was removed (see
+ # governance/hardening-convention.md — design-first prerequisites).
+ - name: Flip approved on the default branch (deploy key)
+ if: steps.decide.outputs.flip == 'true'
+ env:
+ SSH_KEY: ${{ secrets.DESIGN_APPROVAL_DEPLOY_KEY }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ FEATURE: ${{ steps.decide.outputs.feature }}
+ FLOW: ${{ steps.decide.outputs.flow }}
+ AUTHOR: ${{ steps.decide.outputs.author }}
+ STAMP: ${{ steps.decide.outputs.stamp }}
+ MANIFEST: ${{ steps.decide.outputs.manifest_path }}
+ ISSUE: ${{ github.event.issue.number }}
+ run: |
+ set -euo pipefail
+ if [ -z "${SSH_KEY:-}" ]; then
+ echo "::error::DESIGN_APPROVAL_DEPLOY_KEY secret is not set — register a push deploy key as a ruleset bypass_actor and store its private half in that secret (see governance/hardening-convention.md)."
+ exit 1
+ fi
+ mkdir -p ~/.ssh
+ printf '%s\n' "$SSH_KEY" > ~/.ssh/design_approval
+ chmod 600 ~/.ssh/design_approval
+ ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
+ export GIT_SSH_COMMAND="ssh -i ~/.ssh/design_approval -o IdentitiesOnly=yes"
+ git config user.name "fuze-design-approve"
+ git config user.email "design-approve@users.noreply.github.com"
+ git remote set-url --push origin "git@github.com:${GITHUB_REPOSITORY}.git"
+ # Read-modify-write against the CURRENT default branch, not this job's
+ # checkout: an approval landing between our fetch and push just retries on
+ # a fresh base.
+ for attempt in 1 2 3; do
+ git fetch origin "$DEFAULT_BRANCH"
+ git checkout --detach --force FETCH_HEAD
+ # Idempotent flip of exactly the named flow. If it's already approved on
+ # the fresh default branch (a concurrent duplicate won the race) this is a
+ # no-op and we exit success — a double-submit is not a second approval.
+ if ! node -e '
+ const fs = require("fs");
+ const p = process.env.MANIFEST;
+ const m = JSON.parse(fs.readFileSync(p, "utf8"));
+ const flow = (m.build && m.build.flows || []).find(f => f.id === process.env.FLOW);
+ if (!flow) { console.error("flow gone on the fresh default branch: " + process.env.FLOW); process.exit(3); }
+ if (flow.approved === true) { console.log("noop"); process.exit(0); }
+ flow.approved = true;
+ flow.approvedBy = process.env.AUTHOR;
+ flow.approvedAt = new Date().toISOString();
+ fs.writeFileSync(p, JSON.stringify(m, null, 2) + "\n");
+ console.log("flipped");
+ '; then
+ echo "::error::could not flip ${FLOW} in ${MANIFEST}"
+ exit 1
+ fi
+ if git diff --quiet -- "$MANIFEST"; then
+ echo "already approved on ${DEFAULT_BRANCH} — nothing to push"
+ exit 0
+ fi
+ git commit \
+ -m "design(approve): ${FEATURE}/${FLOW} approved by @${AUTHOR} (stamp ${STAMP:0:12}) [skip ci]" \
+ -m "Recorded from design-approval issue #${ISSUE}." \
+ -- "$MANIFEST"
+ if git push origin "HEAD:${DEFAULT_BRANCH}"; then
+ echo "flipped via commit $(git rev-parse HEAD)"
+ exit 0
+ fi
+ echo "push rejected (attempt ${attempt}) — refetching ${DEFAULT_BRANCH} and retrying"
+ done
+ echo "::error::could not push the approval after 3 attempts (${DEFAULT_BRANCH} moving too fast, or the DeployKey bypass/deploy key was removed)"
+ exit 1
+
+ - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ if: steps.decide.outputs.flip == 'true'
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const issueNumber = context.payload.issue.number;
+ const feature = ${{ toJSON(steps.decide.outputs.feature) }};
+ const flowId = ${{ toJSON(steps.decide.outputs.flow) }};
+ const author = ${{ toJSON(steps.decide.outputs.author) }};
+ const stamp = ${{ toJSON(steps.decide.outputs.stamp) }};
+ await github.rest.issues.createComment({
+ owner, repo, issue_number: issueNumber,
+ body: `Approved: flow \`${flowId}\` of \`${feature}\` is now recorded \`approved: true\` (by @${author}, stamp \`${stamp}\`). Committed to the default branch via the deploy-key bypass. Closing this approval request.`,
+ });
+ await github.rest.issues.update({ owner, repo, issue_number: issueNumber, state: 'closed' });
diff --git a/.github/workflows/design-review-notify.yml b/.github/workflows/design-review-notify.yml
new file mode 100644
index 0000000..e7a4353
--- /dev/null
+++ b/.github/workflows/design-review-notify.yml
@@ -0,0 +1,33 @@
+name: Design Review — Notify via Telegram
+
+# Fires when any issue is labeled `design-review`.
+# Sends a Telegram message with an inline link to the issue so the product
+# owner can review design frames and comment `@claude approve` or
+# `@claude reject: ` to trigger the next Claude Code cloud session.
+
+on:
+ issues:
+ types: [labeled]
+
+jobs:
+ notify:
+ if: github.event.label.name == 'design-review'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Send Telegram design-review notification
+ uses: appleboy/telegram-action@2efd23283ae25229a0b4d315d5d33f73b346ef99 # v0.1.1
+ with:
+ to: ${{ secrets.TELEGRAM_CHAT_ID }}
+ token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
+ format: markdown
+ message: |
+ 🎨 *Design Review Required*
+
+ *Issue:* [${{ github.event.issue.title }}](${{ github.event.issue.html_url }})
+ *Repo:* `${{ github.repository }}`
+ *Opened by:* ${{ github.event.issue.user.login }}
+
+ Review the design frames in the issue, then comment on the issue:
+ • `@claude approve` — continue implementation
+ • `@claude reject: ` — send back for revision
diff --git a/.github/workflows/gate-frames-stamped.yml b/.github/workflows/gate-frames-stamped.yml
new file mode 100644
index 0000000..1d1284d
--- /dev/null
+++ b/.github/workflows/gate-frames-stamped.yml
@@ -0,0 +1,36 @@
+name: gate-frames-stamped
+
+# A PR that touches design/frames/** must carry an up-to-date `stamp` in each
+# feature manifest. The stamp is DERIVED from the files (scripts/stamp-frames.mjs);
+# this gate recomputes it and fails if any manifest's recorded stamp is stale.
+# That is what stops an approval binding to frames that changed after stamping —
+# without it the stamp is a hand-maintained mirror that silently drifts.
+
+on:
+ pull_request:
+ paths:
+ - 'design/frames/**'
+ - 'scripts/stamp-frames.mjs'
+ - '.github/workflows/gate-frames-stamped.yml'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: gate-frames-stamped-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ stamped:
+ runs-on: ubuntu-latest
+ steps:
+ # Actions pinned to immutable commit SHAs (repo hardening standard — a mutable
+ # tag can be silently repointed by the action owner).
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '24.x'
+
+ - name: Verify every feature manifest stamp is current
+ run: node scripts/stamp-frames.mjs --check
diff --git a/.github/workflows/gate-line-endings.yml b/.github/workflows/gate-line-endings.yml
index 080c208..1dc786b 100644
--- a/.github/workflows/gate-line-endings.yml
+++ b/.github/workflows/gate-line-endings.yml
@@ -50,7 +50,7 @@ jobs:
gate-line-endings:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
diff --git a/.github/workflows/governance-nightly.yml b/.github/workflows/governance-nightly.yml
index 1dce54c..145e433 100644
--- a/.github/workflows/governance-nightly.yml
+++ b/.github/workflows/governance-nightly.yml
@@ -25,7 +25,7 @@ jobs:
reconcile:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
@@ -49,6 +49,25 @@ jobs:
4. DRIFT — compare .claude/agents + CLAUDE.md + the workflow stack against the expectation in
.fuze/manifest.json and the FuzeSDLC canonical; open a fix PR for mechanical drift, flag
judgment calls.
+ 5. STUB EXPERTS — scan .claude/agents/ for any expert file whose frontmatter contains
+ `stub: true` AND whose name matches manifest.expert (the repo's OWN expert, not advisors).
+ For each: open a tracked issue titled "Expert agent : hub-authored stub — replace with
+ repo-authored content" with: a link to governance/agent-ownership.md, the instruction to run
+ platform-governance locally with prompt "author the -expert for this repo", and a
+ reminder that publish-expert.yml will propagate it. Check for an existing open issue with
+ the same title first (idempotent). Do NOT file this issue for advisor copies.
+ 6. EXPERT SELF-REFRESH — for THIS repo's own expert (manifest.expert):
+ a. Read the current expert file (.claude/agents/.md).
+ b. Read every file/directory listed under manifest.expertMonitors (if the key is present).
+ c. Compare: does the expert's content still accurately describe what those files show?
+ Focus on: architecture changes, new deploy targets, new CI workflows, renamed files,
+ new gotchas, removed components.
+ d. If significant drift is found: open a PR titled "chore(expert): auto-refresh
+ — " on branch expert-refresh/- with a targeted edit
+ to the expert file. Include in the PR body which monitored files changed vs the
+ expert's claims, and what specifically was updated.
+ e. If no drift: skip silently (no PR, no comment).
+ This is non-blocking — never fail a gate on expert freshness.
Then open or UPDATE a single issue titled "Governance reconciliation — "
(label: governance) with: branch actions, per-PR blocker+next-action, per-issue disposition,
diff --git a/.github/workflows/governance-sync.yml b/.github/workflows/governance-sync.yml
index 3b8e704..9885565 100644
--- a/.github/workflows/governance-sync.yml
+++ b/.github/workflows/governance-sync.yml
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout (PR head)
- uses: actions/checkout@v4
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -59,14 +59,14 @@ jobs:
- name: Fetch FuzeSDLC canonical (read-only key)
if: steps.guard.outputs.ok == 'true'
- uses: actions/checkout@v4
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: izzywdev/FuzeSDLC
ref: ${{ steps.ref.outputs.ref }}
ssh-key: ${{ secrets.FUZESDLC_DEPLOY_KEY }}
path: .fuzesdlc-canonical
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
if: steps.guard.outputs.ok == 'true'
with:
python-version: "3.12"
diff --git a/.github/workflows/harden-gate.yml b/.github/workflows/harden-gate.yml
index a4342f1..78a68b2 100644
--- a/.github/workflows/harden-gate.yml
+++ b/.github/workflows/harden-gate.yml
@@ -21,13 +21,46 @@ concurrency:
cancel-in-progress: true
jobs:
+ gate-manifest:
+ # Validate .fuze/manifest.json against the repo manifest schema.
+ #
+ # ENFORCING FROM DAY ONE, which is only defensible because the schema was brought into
+ # line with reality first. Before that: 19 of 22 real manifests violated it, because the
+ # schema declared `additionalProperties: false` while every repo carried a2a/mcp/mobile/
+ # portal blocks it had never heard of. After: 22 of 22 pass. Turning a gate on is safe
+ # exactly when the fleet already satisfies it — otherwise you get 19 permanently-red repos
+ # and a `|| true` two weeks later.
+ #
+ # It caught a real defect on its first run: FuzeFinance declared
+ # channels: ["mcp","mobile"] where the enum member is "mobile-app", backed by neither
+ # block. A typo in a CLOSED enum survived indefinitely because nothing in the family had
+ # ever validated this file — the only reference to the schema in any executable was a
+ # filename inside an error string.
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ with: { python-version: '3.12' }
+ - name: Manifest schema gate (enforcing)
+ shell: bash
+ run: |
+ if [ ! -f scripts/gate_manifest.py ]; then
+ echo "::error title=gate-manifest::scripts/gate_manifest.py is missing. sdlc-bootstrap installs it together with .fuze/repo-manifest.schema.json and this job. Do NOT 'fix' this by skipping."
+ exit 1
+ fi
+ # jsonschema gives full validation. The gate runs without it too — a structural
+ # fallback covering required keys, closed enums and unknown properties — and SAYS
+ # which mode it used, so a degraded run is never mistaken for a clean one.
+ pip install -q jsonschema || echo "::warning::jsonschema unavailable — gate-manifest will run in structural fallback mode"
+ python scripts/gate_manifest.py .
+
gate-lint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v7
- with: { node-version: '20' }
- - uses: actions/setup-python@v5
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with: { node-version: '24.x' }
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with: { python-version: '3.12' }
- name: Lint (adaptive, report-only)
shell: bash
@@ -47,10 +80,10 @@ jobs:
gate-test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v7
- with: { node-version: '20' }
- - uses: actions/setup-python@v5
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with: { node-version: '24.x' }
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with: { python-version: '3.12' }
- name: Test (adaptive, report-only)
shell: bash
@@ -67,9 +100,9 @@ jobs:
gate-build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v7
- with: { node-version: '20' }
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with: { node-version: '24.x' }
- name: Build (adaptive, report-only)
shell: bash
run: |
@@ -82,14 +115,14 @@ jobs:
gate-sast:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Semgrep (report-only, SARIF)
shell: bash
run: |
pip install -q semgrep
semgrep scan --config auto --sarif --output semgrep.sarif || true
test -f semgrep.sarif || echo '{"version":"2.1.0","runs":[]}' > semgrep.sarif
- - uses: github/codeql-action/upload-sarif@v4
+ - uses: github/codeql-action/upload-sarif@v3
continue-on-error: true # private repos lack code-scanning; report-only must stay green
if: always()
with:
@@ -99,17 +132,61 @@ jobs:
gate-secret-scan:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 }
+
+ # Runs the gitleaks BINARY, not gitleaks-action.
+ #
+ # The action's pull_request path calls GET /repos/:o/:r/pulls/:n/commits to work out
+ # what to scan, and when that call fails it does not degrade — it throws, and the job
+ # dies before scanning a single byte. That is happening now: the call returns 403
+ # "Resource not accessible by integration" while the job's own token summary prints
+ # "PullRequests: read", so the permission the API asks for is the permission the token
+ # already has. It broke repo-wide between 2026-08-11 and 2026-08-17: three unrelated
+ # dependabot PRs and this one all failed inside a few hours, on branches that had
+ # passed the same gate days earlier.
+ #
+ # A secret gate that depends on an API call is a gate with an availability failure
+ # mode, and the whole point of this one is that it CANNOT be the thing that quietly
+ # stops working. git already knows the commit range; nothing needs to be asked.
- name: gitleaks (gates new secrets)
- uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
+ shell: bash
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ set -euo pipefail
+
+ VERSION=8.24.3
+ SHA256=9991e0b2903da4c8f6122b5c3186448b927a5da4deef1fe45271c3793f4ee29c
+ TARBALL=gitleaks_${VERSION}_linux_x64.tar.gz
+
+ # Pinned + checksummed. A security gate that curls an unverified binary is worse
+ # than the problem it is solving; the digest is the upstream one from
+ # gitleaks_${VERSION}_checksums.txt.
+ curl -sSfL -o "$TARBALL" \
+ "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/${TARBALL}"
+ echo "${SHA256} ${TARBALL}" | sha256sum -c -
+ tar -xzf "$TARBALL" gitleaks
+ sudo install -m 0755 gitleaks /usr/local/bin/gitleaks
+ rm -f "$TARBALL" gitleaks
+
+ # On a PR, scan exactly the commits the PR adds. Anywhere else, scan all history:
+ # a wider scan can only find MORE, so the fallback is the safe direction. There is
+ # deliberately no path that scans nothing — "no range, so skip" is how a gate ends
+ # up green while enforcing nothing.
+ if [ -n "${BASE_SHA:-}" ] && [ -n "${HEAD_SHA:-}" ]; then
+ echo "::notice title=gate-secret-scan::scanning ${BASE_SHA}..${HEAD_SHA}"
+ gitleaks git --log-opts="${BASE_SHA}..${HEAD_SHA}" --redact --no-banner .
+ else
+ echo "::notice title=gate-secret-scan::no PR range — scanning full history"
+ gitleaks git --redact --no-banner .
+ fi
gate-dependency-scan:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Trivy filesystem scan (report-only, SARIF)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
@@ -119,7 +196,7 @@ jobs:
output: trivy.sarif
severity: CRITICAL,HIGH
exit-code: '0'
- - uses: github/codeql-action/upload-sarif@v4
+ - uses: github/codeql-action/upload-sarif@v3
continue-on-error: true # private repos lack code-scanning; report-only must stay green
if: always()
with:
@@ -131,7 +208,7 @@ jobs:
# ratchets to enforcing per-repo once the rules are tuned green (see architecture-guidelines.md).
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 }
- name: Semgrep authz/appsec scan (report-only, SARIF)
shell: bash
@@ -141,20 +218,149 @@ jobs:
[ -f .semgrep/fuze-authz.yml ] && CFG="$CFG --config .semgrep/fuze-authz.yml"
semgrep scan $CFG --config p/owasp-top-ten --config p/secrets --sarif --output authz.sarif || true
test -f authz.sarif || echo '{"version":"2.1.0","runs":[]}' > authz.sarif
- - uses: github/codeql-action/upload-sarif@v4
+ - uses: github/codeql-action/upload-sarif@v3
if: always()
continue-on-error: true
with:
sarif_file: authz.sarif
category: authz
+ gate-pagination:
+ # Pagination standard (CLAUDE.baseline.md §4.1). Inspects OpenAPI/Swagger contracts and
+ # FAILS when an unbounded collection GET lacks limit + cursor|offset + the {items,page}
+ # envelope, unless annotated `x-pagination: exempt` or allowlisted. Report-only first pass
+ # (|| true) — flip to enforcing per-repo once contracts are annotated. Owned by contract-designer
+ # (contract) + backend-engineer (impl) + test-engineer (verify); policy by platform-governance.
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ with: { python-version: '3.12' }
+ - name: Pagination gate
+ shell: bash
+ run: |
+ pip install -q pyyaml
+ SCRIPT=""
+ for c in scripts/gate_pagination.py .fuze/gate_pagination.py .github/scripts/gate_pagination.py; do
+ [ -f "$c" ] && SCRIPT="$c" && break
+ done
+ if [ -z "$SCRIPT" ]; then
+ echo "::warning title=gate-pagination::no gate_pagination.py found in repo — skipping"; exit 0
+ fi
+ python "$SCRIPT" . || true # report-only first pass; remove "|| true" to enforce
+
+ gate-identifier:
+ # Server-owned entity identifiers (governance/identifier-standard.md).
+ #
+ # THIS JOB DID NOT EXIST IN THIS TEMPLATE UNTIL NOW, AND THAT WAS THE GAP.
+ # CLAUDE.baseline.md §4.2 declares the standard normative and says it is
+ # "enforced by gate-identifier" — but the rule shipped as two docs(baseline)
+ # commits carrying no script and no job. Measured result across the fleet:
+ # 0 repos ran the gate, 0 declared `identity.namespace`, 0 depended on
+ # @izzywdev/fuzefront-identity or fuzefront-identity. The standard travelled;
+ # the enforcement did not.
+ #
+ # Note the ordering of the checks below is deliberate. Two of them
+ # (--registry-parity, --namespace) return [] early when the files they read
+ # are absent, so a repo that has adopted nothing passes them VACUOUSLY.
+ # --adoption is the check that actually fires on non-adoption, and its own
+ # docstring names the reason it exists: "a family standard whose gate is
+ # satisfied by non-adoption is not a standard."
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ with: { python-version: '3.12' }
+
+ - name: Identifier gate — self-tests (the gate must be known to FIRE)
+ shell: bash
+ # Asserts the gate FAILS on a violation, not merely that it passes on a
+ # clean tree. `claude-auto-pr.yml` was green for its whole life because
+ # every run took its early-exit path; the one time it had real work, it
+ # failed. Passing is not evidence. These run FIRST so a broken gate
+ # reports as a broken gate rather than as a clean repo.
+ run: |
+ if [ ! -f scripts/gate_identifier.py ]; then
+ echo "::error title=gate-identifier::scripts/gate_identifier.py is missing. It is installed by sdlc-bootstrap together with its self-tests, the allowlist and this job — if you have this job you must have the script. Do NOT 'fix' this by skipping."
+ exit 1
+ fi
+ # Pattern is test_*.py, NOT test_gate_identifier*.py. An earlier version of this
+ # step scoped the glob to the identifier tests, which would have meant any OTHER
+ # suite added to scripts/__tests__/ (the bootstrap installer's, for one) silently
+ # never ran — a new vacuous gate, introduced by the commit that removed four of
+ # them. Discovery here is deliberately broad: this directory holds the self-tests
+ # for every distributed script, and all of them must be known to fire.
+ python -m unittest discover -s scripts/__tests__ -p 'test_*.py'
+
+ - name: Identifier gate — contracts (enforcing)
+ shell: bash
+ # C1 no client-supplied id on create, C2 additionalProperties:false,
+ # C3 polymorphic reference carries its type, C4 aggregate scoping.
+ # Enforcing from day one: these fire only on NEW violations in specs the
+ # repo already has, so a repo that has adopted nothing is not penalised.
+ run: |
+ pip install -q pyyaml
+ python scripts/gate_identifier.py .
+ python scripts/gate_identifier.py . --registry-parity
+ python scripts/gate_identifier.py . --namespace
+
+ - name: Identifier gate — adoption
+ shell: bash
+ # ENFORCING ONLY ONCE THE REPO OPTS IN, by declaring `identity` in
+ # .fuze/manifest.json. That is deliberate and is not the same thing as
+ # "report-only until someone remembers":
+ # * a repo that has NOT declared identity gets a notice, not a failure
+ # — installing this enforcing everywhere would red most of the fleet
+ # on day one for a standard they have not adopted yet;
+ # * a repo that HAS declared it is asserting it adopted the package, so
+ # the check is enforcing immediately. There is no state in which a
+ # repo claims adoption and the gate stays quiet.
+ run: |
+ if python3 -c "import json,sys; sys.exit(0 if 'identity' in json.load(open('.fuze/manifest.json')) else 1)" 2>/dev/null; then
+ echo "manifest declares identity — adoption check is ENFORCING"
+ python scripts/gate_identifier.py . --adoption
+ else
+ echo "::notice title=gate-identifier::.fuze/manifest.json declares no \`identity\` block, so this repo has not opted into the identifier standard. Adding that block makes the adoption check enforcing. See governance/identifier-standard.md."
+ fi
+
+ gate-ds-conformance:
+ # Design-system conformance (CLAUDE.baseline.md §6). HARD-FAILS raw design values in feature
+ # code (hex/rgb/hsl/px/font outside tokens, excludes the DS package). SEPARATELY opens one
+ # idempotent `ds-extraction` GitHub issue per recurring UI pattern that should become a DS
+ # primitive. Report-only first pass (|| true). Owned by frontend-engineer (sole DS owner).
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ with: { python-version: '3.12' }
+ - name: DS conformance gate (+ extraction issues on push to default branch)
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ run: |
+ SCRIPT=""
+ for c in scripts/gate_ds_conformance.py .fuze/gate_ds_conformance.py .github/scripts/gate_ds_conformance.py; do
+ [ -f "$c" ] && SCRIPT="$c" && break
+ done
+ if [ -z "$SCRIPT" ]; then
+ echo "::warning title=gate-ds-conformance::no gate_ds_conformance.py found in repo — skipping"; exit 0
+ fi
+ EMIT=""
+ # Only emit extraction issues on push to the default branch (not on every PR) to avoid noise.
+ if [ "${{ github.event_name }}" = "push" ]; then EMIT="--emit-issues"; fi
+ python "$SCRIPT" . $EMIT || true # report-only first pass; remove "|| true" to enforce hard violations
+
gate-localup:
# Bounded local deployability. Stands up FuzeInfra consumer-test (if vendored) + validates the
# Helm chart; warns (tracked) when no bounded local-up is wired yet. local-env-verifier ratchets
# to enforcing per-repo (see local-environment.md).
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { submodules: recursive }
- name: Consumer-test bounded up + smoke
shell: bash
@@ -178,13 +384,54 @@ jobs:
curl -sSL https://get.helm.sh/helm-v3.16.4-linux-amd64.tar.gz | tar xz -C /tmp >/dev/null 2>&1 && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm || true
helm lint "$cdir" || echo "::warning title=helm-lint::chart findings (report)"
+ gate-toolchain:
+ # Family toolchain floor (Node 24 / React 19 — see the "Toolchain baseline" convention
+ # documented per-repo, originated in FuzeFront's CLAUDE.md and generalized here). A sweep
+ # found several repos on node:18/20-alpine Docker images and CI runners with nothing
+ # checking it. Dependency-free (node scripts/gate-toolchain.mjs); safe to run on any repo
+ # — it scans package.json/.nvmrc/Dockerfiles/workflows/Module-Federation configs and
+ # passes vacuously (0 files scanned) on a repo with none of those. Report-only first pass
+ # (|| true): several repos are known non-compliant today (see the originating issue) and
+ # a hard-fail here would block every unrelated PR on them the moment this template syncs.
+ # Ratchet to enforcing per repo once its own bump PR (adding the missing engines/
+ # @types/node/Dockerfile-major/requiredVersion fields) has landed.
+ #
+ # CORRECTION: an earlier version of this comment claimed "FuzeFront's own copy of this job
+ # has already done so and dropped the || true". That was not true and is worth recording
+ # rather than quietly deleting, because it gave false confidence in an unproven gate:
+ # FuzeFront has NO gate-toolchain job and NO gate-toolchain.mjs — `grep -c toolchain` over
+ # its .github/workflows/ returns 0. Combined with the fact that sdlc-bootstrap distributes
+ # no scripts beyond the two frames .mjs files, this gate has never executed anywhere. The
+ # scan below is therefore UNPROVEN in production; treat its first real run as a smoke test.
+ #
+ # The floor it checks is declared data, not a constant in the script: FuzeFront's
+ # .fuze/manifest.json already carries a `toolchain` block (node/npm/typesNode/react/
+ # reactPeerRange/typesReact/moduleFederationRequiredVersion/dockerBaseImage/ciNodeVersion).
+ # That block is the canonical shape — promote it, do not invent a second one.
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with: { node-version: '24.x' }
+ - name: Toolchain floor gate (report-only)
+ shell: bash
+ run: |
+ SCRIPT=""
+ for c in scripts/gate-toolchain.mjs .fuze/gate-toolchain.mjs .github/scripts/gate-toolchain.mjs; do
+ [ -f "$c" ] && SCRIPT="$c" && break
+ done
+ if [ -z "$SCRIPT" ]; then
+ echo "::warning title=gate-toolchain::no gate-toolchain.mjs found in repo — skipping"; exit 0
+ fi
+ node "$SCRIPT" || true # report-only first pass; remove "|| true" once this repo is compliant
+
gate-version:
# SemVer discipline (governance/versioning.md): a changed npm package or API contract
# must bump its version. Report-only first pass; ratchet to enforcing per repo.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { fetch-depth: 0 }
- name: SemVer bump check (packages + API contracts, report-only)
shell: bash
diff --git a/.github/workflows/helm-validate.yml b/.github/workflows/helm-validate.yml
index 1141ecf..6a31346 100644
--- a/.github/workflows/helm-validate.yml
+++ b/.github/workflows/helm-validate.yml
@@ -2,35 +2,29 @@ name: Helm Chart Validation
on:
push:
- branches: [main]
+ branches: [main, develop, staging]
paths:
+ # Both layouts — see the discovery step below for why the split exists and
+ # why it is not normalized. Watching only helm/** meant a chart change in a
+ # deploy/helm/ repo never even triggered this workflow.
+ - 'helm/**'
- 'deploy/helm/**'
- '.github/workflows/helm-validate.yml'
pull_request:
branches: [main]
- paths:
- - 'deploy/helm/**'
- - '.github/workflows/helm-validate.yml'
jobs:
validate:
name: Lint & schema-validate chart
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up Helm
- uses: azure/setup-helm@v4
+ uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
with:
version: v3.16.4
- - name: Helm lint (all overlays)
- run: |
- helm lint deploy/helm/fuzeagent
- helm lint deploy/helm/fuzeagent -f deploy/helm/fuzeagent/values-prod.yaml
- helm lint deploy/helm/a2a-shared
- helm lint deploy/helm/a2a-shared -f deploy/helm/a2a-shared/values-prod.yaml
-
- name: Install kubeconform
run: |
curl -sSLo /tmp/kubeconform.tar.gz \
@@ -38,21 +32,55 @@ jobs:
tar -xzf /tmp/kubeconform.tar.gz -C /tmp
sudo install /tmp/kubeconform /usr/local/bin/kubeconform
- - name: Render & validate (default + prod overlays)
+ # Auto-discovers every chart so this template works verbatim in any repo
+ # (do NOT hardcode a chart name — an earlier template hardcoded
+ # helm/fuzeinfra and failed in every consuming repo with "no such file").
+ #
+ # BOTH LAYOUTS ARE DISCOVERED, and that is the whole point of this step.
+ # The family is genuinely split — 10 clones keep charts under deploy/helm/
+ # (FuzeCall, FuzeDeploy, FuzeHub, FuzePicker, FuzePlan, FuzeSocial,
+ # FuzeMarket, FuzeKeys, FuzeX, FuzeAgent) and the rest under helm/. This
+ # step used to glob helm/*/Chart.yaml only and `exit 0` when it found
+ # nothing, so every one of those repos got a GREEN "Helm Chart Validation"
+ # that had validated no chart at all — the failure mode is indistinguishable
+ # from success, which is why it survived so long.
+ #
+ # The layouts are NOT normalized deliberately: an Argo Application pins
+ # `spec.source.path` (e.g. fuzeservice/deploy/argocd/application.yaml ->
+ # `path: helm/fuzeservice`), so moving a chart silently breaks prod sync.
+ # Fix discovery, never the layout.
+ - name: Lint & validate all charts (helm/ and deploy/helm/)
run: |
- # -ignore-missing-schemas: Traefik / SealedSecret CRDs have no published
- # JSON schema; matches FuzeInfra's helm-validate + deploy-prod convention.
- for overlay in values.yaml values-prod.yaml; do
- echo "::group::fuzeagent $overlay"
- helm template fuzeagent deploy/helm/fuzeagent -f "deploy/helm/fuzeagent/$overlay" \
- | kubeconform -strict -summary -kubernetes-version 1.29.0 -ignore-missing-schemas
+ shopt -s nullglob
+ charts=(helm/*/Chart.yaml deploy/helm/*/Chart.yaml)
+ if [ ${#charts[@]} -eq 0 ]; then
+ # A repo that declares a chart capability but ships no chart is a
+ # defect, not a no-op. Only a repo with NEITHER directory is exempt.
+ if [ -d helm ] || [ -d deploy/helm ]; then
+ echo "::error title=helm-validate::a chart directory exists but contains no */Chart.yaml — this repo declares a chart it does not ship"
+ ls -la helm deploy/helm 2>/dev/null || true
+ exit 1
+ fi
+ echo "No helm/ or deploy/helm/ directory — this repo ships no chart; nothing to validate."
+ exit 0
+ fi
+ echo "Discovered ${#charts[@]} chart(s): ${charts[*]}"
+ for chartyaml in "${charts[@]}"; do
+ chart="$(dirname "$chartyaml")"
+ name="$(basename "$chart")"
+ echo "::group::helm lint $chart"
+ helm lint "$chart"
echo "::endgroup::"
- done
- # a2a-shared ships enabled:false (default/prod render nothing); the ci/
- # overlay exercises the enabled Deployment/Service/ConfigMap/Ingress path.
- for overlay in values.yaml values-prod.yaml ci/enabled-values.yaml; do
- echo "::group::a2a-shared $overlay"
- helm template a2a-shared deploy/helm/a2a-shared -f "deploy/helm/a2a-shared/$overlay" \
+ echo "::group::template + kubeconform $chart (base values)"
+ helm template "$name" "$chart" \
| kubeconform -strict -summary -kubernetes-version 1.29.0 -ignore-missing-schemas
echo "::endgroup::"
+ for overlay in "$chart"/values-*.yaml; do
+ [ -e "$overlay" ] || continue
+ echo "::group::overlay $(basename "$overlay")"
+ helm lint "$chart" -f "$overlay"
+ helm template "$name" "$chart" -f "$overlay" \
+ | kubeconform -strict -summary -kubernetes-version 1.29.0 -ignore-missing-schemas
+ echo "::endgroup::"
+ done
done
diff --git a/.github/workflows/label-sync.yml b/.github/workflows/label-sync.yml
new file mode 100644
index 0000000..4a0cfc4
--- /dev/null
+++ b/.github/workflows/label-sync.yml
@@ -0,0 +1,71 @@
+name: Label Sync
+
+# Upsert the FuzeSDLC governance label set from the in-repo manifest (.github/labels.json).
+#
+# WHY: the auto-merge / branch-lifecycle / design-first workflows gate on LABELS (e.g.
+# auto-merge.yml only merges a PR carrying the `auto-merge` label). Labels are a GitHub API
+# resource, not a file, so governance-sync (which reconciles FILES) never provisioned them —
+# they drifted per-repo. This makes the label set a propagated, self-healing artifact:
+# whenever .github/labels.json lands on the default branch, every label is created-or-updated.
+#
+# SELF-CONTAINED BY DESIGN (same rationale as governance-sync): no cross-repo `uses:` — the
+# upsert logic is inlined so a private-hub Actions-access setting can never red the run.
+# Non-destructive: `gh label create --force` upserts; labels absent from the manifest (repo-
+# specific ones) are left untouched. sdlc-bootstrap rewrites `branches: [main]` to the repo's
+# real default branch.
+
+on:
+ push:
+ branches: [main]
+ paths: ['.github/labels.json']
+ workflow_dispatch:
+
+permissions:
+ # BOTH are required, and omitting `contents` is not a smaller grant — it is a
+ # REVOCATION. A `permissions:` block sets GITHUB_TOKEN's scopes to exactly what
+ # it lists; everything unlisted becomes `none`. With `issues: write` alone,
+ # `contents` is none and actions/checkout below cannot read the repository.
+ #
+ # On a PRIVATE repo that surfaces as:
+ # remote: Repository not found.
+ # fatal: repository 'https://github.com///' not found
+ # — a 404, not a permission error, because an unauthorized reader must not be
+ # able to distinguish "exists but forbidden" from "does not exist". So the log
+ # reads as if the repository had been deleted and nothing points at this block.
+ #
+ # That is also why it shipped: checkout of a PUBLIC repo succeeds WITHOUT the
+ # contents scope, so the omission stays invisible until a private repo adopts
+ # the template. Observed on izzywdev/FuzeExecutive (private), where every run
+ # since install had failed this way. Fixed downstream in FuzeExecutive#28.
+ contents: read # actions/checkout
+ issues: write # labels are managed under the issues scope
+
+jobs:
+ label-sync:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - name: Upsert labels from .github/labels.json
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ if [ ! -f .github/labels.json ]; then
+ echo "::notice::no .github/labels.json — nothing to sync"; exit 0
+ fi
+ python3 - <<'PY'
+ import json, subprocess
+ labels = json.load(open(".github/labels.json", encoding="utf-8"))
+ ok = err = 0
+ for l in labels:
+ cmd = ["gh", "label", "create", l["name"],
+ "--color", str(l.get("color", "ededed")).lstrip("#"), "--force"]
+ if l.get("description"):
+ cmd += ["--description", l["description"]]
+ r = subprocess.run(cmd, capture_output=True, text=True)
+ if r.returncode == 0:
+ ok += 1; print(f" + {l['name']}")
+ else:
+ err += 1; print(f" ! {l['name']}: {r.stderr.strip()}")
+ print(f"labels synced: {ok} ok, {err} failed")
+ raise SystemExit(1 if err else 0)
+ PY
diff --git a/.github/workflows/mcp-maintain.yml b/.github/workflows/mcp-maintain.yml
index 0498686..04866c1 100644
--- a/.github/workflows/mcp-maintain.yml
+++ b/.github/workflows/mcp-maintain.yml
@@ -49,7 +49,12 @@ jobs:
- name: Checkout PR head (writable, same-repo only)
if: steps.guard.outputs.ok == 'true'
- uses: actions/checkout@v4
+ # Pinned to a full commit SHA, not a mutable tag: a tag can be silently
+ # repointed by the action owner (cf. the trivy-action / kics-github-action
+ # compromises). This workflow holds `contents: write` on the PR branch, so a
+ # repointed checkout would run attacker code with push access. Semgrep's
+ # github-actions-mutable-action-tag rule flags the unpinned form.
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/nightly-integration.yml b/.github/workflows/nightly-integration.yml
index 03b6d54..3f8036f 100644
--- a/.github/workflows/nightly-integration.yml
+++ b/.github/workflows/nightly-integration.yml
@@ -12,7 +12,7 @@ name: Nightly Integration
on:
schedule:
- - cron: '27 4 * * *' # off-peak, off the :00 herd; per-repo offset set at install time
+ - cron: '37 8 * * *' # off-peak, off the :00 herd; per-repo offset set at install time
workflow_dispatch: # manual run
permissions:
@@ -32,7 +32,7 @@ jobs:
outputs:
ran: ${{ steps.detect.outputs.ran }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
submodules: recursive
fetch-depth: 0
@@ -126,7 +126,7 @@ jobs:
!startsWith(github.ref_name, 'nightly-autofix-') }}
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
- name: Auto-fix → DRAFT PR (never auto-merge)
diff --git a/.github/workflows/pages-frames.yml b/.github/workflows/pages-frames.yml
new file mode 100644
index 0000000..03cddb3
--- /dev/null
+++ b/.github/workflows/pages-frames.yml
@@ -0,0 +1,57 @@
+name: pages-frames
+
+# Publishes design/frames/** to GitHub Pages so every feature awaiting UX approval
+# has a real, clickable URL: https://.github.io// (derived by the build script from GITHUB_REPOSITORY)
+# The top-level index is DERIVED from the directories present under design/frames/
+# (scripts/build-frames-site.mjs) — never a hand-maintained list.
+
+on:
+ push:
+ branches: [main, master]
+ paths:
+ - 'design/frames/**'
+ - 'scripts/build-frames-site.mjs'
+ - '.github/workflows/pages-frames.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Never cancel an in-flight deployment; Pages allows one at a time.
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ # Actions pinned to immutable commit SHAs (repo hardening standard — a mutable
+ # tag can be silently repointed by the action owner; see the trivy-action /
+ # kics-github-action compromises).
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '24.x'
+
+ - name: Build frames site (index derived from design/frames/*/)
+ run: node scripts/build-frames-site.mjs --out _site
+
+ - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
+
+ - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
+ with:
+ path: _site
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
diff --git a/.github/workflows/publish-expert.yml b/.github/workflows/publish-expert.yml
index daf1be1..da3a3e2 100644
--- a/.github/workflows/publish-expert.yml
+++ b/.github/workflows/publish-expert.yml
@@ -22,6 +22,8 @@ on:
paths:
- ".claude/agents/*-expert.md"
- ".fuze/manifest.json"
+ pull_request:
+ types: [opened, synchronize, reopened]
workflow_dispatch:
permissions:
@@ -30,8 +32,9 @@ permissions:
jobs:
publish:
runs-on: ubuntu-latest
+ if: github.event_name != 'pull_request'
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Guard — token + manifest present
id: guard
@@ -98,3 +101,65 @@ jobs:
--body-file /tmp/pr-body.md
fi
echo "::notice::agent-sync PR open for ${EXPERT}."
+
+ hint-expert-drift:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+
+ - name: Flag expert-drift-risk when PR touches monitored paths
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ if [ ! -f .fuze/manifest.json ]; then
+ echo "::notice::no .fuze/manifest.json — skipping drift hint."; exit 0
+ fi
+ EXPERT=$(python3 -c "import json; print(json.load(open('.fuze/manifest.json')).get('expert',''))")
+ if [ -z "$EXPERT" ]; then
+ echo "::notice::no expert declared — skipping drift hint."; exit 0
+ fi
+ python3 -c "
+ import json, sys
+ d = json.load(open('.fuze/manifest.json'))
+ monitors = d.get('expertMonitors', [])
+ if not monitors:
+ sys.exit(0)
+ open('/tmp/monitors.txt', 'w').write('\n'.join(monitors))
+ " || true
+ if [ ! -f /tmp/monitors.txt ]; then
+ echo "::notice::no expertMonitors declared — skipping drift hint."; exit 0
+ fi
+ # Get changed files in this PR (up to 300 files via pagination)
+ gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate \
+ --jq '.[].filename' > /tmp/changed.txt 2>/dev/null || true
+ if [ ! -s /tmp/changed.txt ]; then
+ echo "::notice::could not fetch PR files — skipping."; exit 0
+ fi
+ MATCHED=$(python3 -c "
+ monitors = [m.strip().rstrip('/') for m in open('/tmp/monitors.txt').read().strip().split('\n') if m.strip()]
+ changed = [f.strip() for f in open('/tmp/changed.txt').read().strip().split('\n') if f.strip()]
+ hits = []
+ for f in changed:
+ for m in monitors:
+ if f == m or f.startswith(m + '/') or f.startswith(m):
+ hits.append(f)
+ break
+ print('\n'.join(hits))
+ ")
+ if [ -z "$MATCHED" ]; then
+ echo "::notice::no overlap with expertMonitors — no drift risk."; exit 0
+ fi
+ echo "::notice::PR touches monitored paths — flagging expert-drift-risk."
+ # Add label (silently ignore if label doesn't exist yet in the repo)
+ gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" \
+ -X POST -f "labels[]=expert-drift-risk" 2>/dev/null || true
+ # Post advisory comment
+ MATCHED_LIST=$(echo "$MATCHED" | sed 's/^/- /')
+ gh pr comment "${PR_NUMBER}" --repo "${REPO}" --body "$(printf '⚠️ **Expert drift risk:** this PR touches files monitored by \`%s\`.\n\nMatched paths:\n%s\n\nConsider updating \`.claude/agents/%s.md\` to reflect any architectural changes. See [governance/agent-ownership.md](governance/agent-ownership.md) for the expert update workflow.' "$EXPERT" "$MATCHED_LIST" "$EXPERT")" 2>/dev/null || true
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..a3919f8
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,35 @@
+# Scope -> registry mapping for the private @fuzefront/* packages.
+#
+# READ THIS BEFORE CHANGING A SCOPE LINE. Publishing RENAMES the packages:
+# scripts/publish-packages.mjs rewrites @fuzefront/ to @izzywdev/fuzefront-,
+# because GitHub Packages requires the scope to match the owning ACCOUNT and there
+# is no `fuzefront` org. So the name you install is NOT the name you import:
+#
+# published @izzywdev/fuzefront-design-system <- what npm fetches
+# imported @fuzefront/design-system <- what the code says
+#
+# A plain `"@fuzefront/design-system": "^1.0.0"` therefore 404s. That is not
+# hypothetical — it is exactly what fuzeplan and fuzefinance have. Depend on it
+# through an npm: alias instead, which keeps every import statement unchanged:
+#
+# "dependencies": {
+# "@fuzefront/design-system": "npm:@izzywdev/fuzefront-design-system@^1.0.0"
+# }
+#
+# @izzywdev is the scope npm actually resolves against the registry; the others
+# are mapped so that any package still carrying an old scope name resolves too
+# rather than silently falling through to the public registry and 404ing.
+@izzywdev:registry=https://npm.pkg.github.com
+@fuzefront:registry=https://npm.pkg.github.com
+@fuzeone:registry=https://npm.pkg.github.com
+
+# Auth comes from the environment — NEVER commit a token. In CI, GITHUB_TOKEN is
+# already present; locally, export a PAT with read:packages.
+//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
+always-auth=true
+
+# WHY THIS FILE ALSO LIVES AT THE REPO ROOT, not only next to the consuming
+# package.json: npm resolves .npmrc by walking UP from the current directory. A
+# repo whose only .npmrc sits in packages// gives no mapping at all to anything
+# run from the root — CI steps, tooling, a fresh clone — and every private scope
+# 404s there. FuzeBI had precisely that shape.
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..a45fd52
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+24
diff --git a/agent-templates/sync/role_loader.py b/agent-templates/sync/role_loader.py
index 9cf3685..d71f87f 100644
--- a/agent-templates/sync/role_loader.py
+++ b/agent-templates/sync/role_loader.py
@@ -4,9 +4,12 @@
followed by the base guardrail block and any role-specific `system_append`.
"""
import json
+import logging
import os
import re
+log = logging.getLogger(__name__)
+
HERE = os.path.dirname(os.path.abspath(__file__))
TEMPLATES_ROOT = os.path.dirname(HERE) # agent-templates/
REPO_ROOT = os.path.dirname(TEMPLATES_ROOT) # repo root (personas live under .claude/agents)
@@ -76,14 +79,29 @@ def agent_payload(manifest):
# or set-but-empty -> "") — the API rejects an empty/invalid url. Also drop the
# matching mcp_toolset so the agent creates cleanly with only its configured servers;
# re-provision after setting the URL to add the server + tool back.
+ #
+ # A drop is NEVER silent (FA-13): a required server logs at WARNING (its tools are
+ # missing until the URL is set), an `optional: true` server logs at INFO. A silent
+ # strip was the worst failure mode — an agent came up tool-less with no signal at all.
servers = expand_env(manifest.get("mcp_servers", []))
valid, dropped = [], set()
for s in servers:
+ # `optional` is our own hint, never part of the API payload — strip it either way.
+ optional = bool(s.pop("optional", False))
url = s.get("url", "")
if url and "${" not in url:
valid.append(s)
+ continue
+ name = s.get("name")
+ dropped.add(name)
+ reason = "url unset/empty" if not url else f"url unresolved ({url!r})"
+ if optional:
+ log.info("MCP server %r on agent %r dropped (optional): %s — continuing without it.",
+ name, manifest.get("name"), reason)
else:
- dropped.add(s.get("name"))
+ log.warning("MCP server %r on agent %r dropped: %s — its tools will be MISSING until "
+ "the URL is configured; re-provision after setting it.",
+ name, manifest.get("name"), reason)
tools = expand_env(manifest.get("tools", []))
if dropped:
tools = [t for t in tools
diff --git a/containers/templates/dev-typescript/.npmrc b/containers/templates/dev-typescript/.npmrc
new file mode 100644
index 0000000..a3919f8
--- /dev/null
+++ b/containers/templates/dev-typescript/.npmrc
@@ -0,0 +1,35 @@
+# Scope -> registry mapping for the private @fuzefront/* packages.
+#
+# READ THIS BEFORE CHANGING A SCOPE LINE. Publishing RENAMES the packages:
+# scripts/publish-packages.mjs rewrites @fuzefront/ to @izzywdev/fuzefront-,
+# because GitHub Packages requires the scope to match the owning ACCOUNT and there
+# is no `fuzefront` org. So the name you install is NOT the name you import:
+#
+# published @izzywdev/fuzefront-design-system <- what npm fetches
+# imported @fuzefront/design-system <- what the code says
+#
+# A plain `"@fuzefront/design-system": "^1.0.0"` therefore 404s. That is not
+# hypothetical — it is exactly what fuzeplan and fuzefinance have. Depend on it
+# through an npm: alias instead, which keeps every import statement unchanged:
+#
+# "dependencies": {
+# "@fuzefront/design-system": "npm:@izzywdev/fuzefront-design-system@^1.0.0"
+# }
+#
+# @izzywdev is the scope npm actually resolves against the registry; the others
+# are mapped so that any package still carrying an old scope name resolves too
+# rather than silently falling through to the public registry and 404ing.
+@izzywdev:registry=https://npm.pkg.github.com
+@fuzefront:registry=https://npm.pkg.github.com
+@fuzeone:registry=https://npm.pkg.github.com
+
+# Auth comes from the environment — NEVER commit a token. In CI, GITHUB_TOKEN is
+# already present; locally, export a PAT with read:packages.
+//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
+always-auth=true
+
+# WHY THIS FILE ALSO LIVES AT THE REPO ROOT, not only next to the consuming
+# package.json: npm resolves .npmrc by walking UP from the current directory. A
+# repo whose only .npmrc sits in packages// gives no mapping at all to anything
+# run from the root — CI steps, tooling, a fresh clone — and every private scope
+# 404s there. FuzeBI had precisely that shape.
diff --git a/containers/templates/dev-typescript/package.json b/containers/templates/dev-typescript/package.json
index f3bb174..7611d12 100644
--- a/containers/templates/dev-typescript/package.json
+++ b/containers/templates/dev-typescript/package.json
@@ -17,7 +17,7 @@
"clean": "rm -rf dist coverage node_modules/.cache"
},
"devDependencies": {
- "@types/node": "^20.0.0",
+ "@types/node": "^24.13.3",
"@types/jest": "^29.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
@@ -35,10 +35,14 @@
"dotenv": "^16.3.0"
},
"engines": {
- "node": ">=18.0.0",
- "npm": ">=9.0.0"
+ "node": ">=24.0.0",
+ "npm": ">=10.0.0"
},
- "keywords": ["fuzeagent", "typescript", "development"],
+ "keywords": [
+ "fuzeagent",
+ "typescript",
+ "development"
+ ],
"author": "FuzeAgent",
"license": "MIT"
}
\ No newline at end of file
diff --git a/e2e/.npmrc b/e2e/.npmrc
new file mode 100644
index 0000000..a3919f8
--- /dev/null
+++ b/e2e/.npmrc
@@ -0,0 +1,35 @@
+# Scope -> registry mapping for the private @fuzefront/* packages.
+#
+# READ THIS BEFORE CHANGING A SCOPE LINE. Publishing RENAMES the packages:
+# scripts/publish-packages.mjs rewrites @fuzefront/ to @izzywdev/fuzefront-,
+# because GitHub Packages requires the scope to match the owning ACCOUNT and there
+# is no `fuzefront` org. So the name you install is NOT the name you import:
+#
+# published @izzywdev/fuzefront-design-system <- what npm fetches
+# imported @fuzefront/design-system <- what the code says
+#
+# A plain `"@fuzefront/design-system": "^1.0.0"` therefore 404s. That is not
+# hypothetical — it is exactly what fuzeplan and fuzefinance have. Depend on it
+# through an npm: alias instead, which keeps every import statement unchanged:
+#
+# "dependencies": {
+# "@fuzefront/design-system": "npm:@izzywdev/fuzefront-design-system@^1.0.0"
+# }
+#
+# @izzywdev is the scope npm actually resolves against the registry; the others
+# are mapped so that any package still carrying an old scope name resolves too
+# rather than silently falling through to the public registry and 404ing.
+@izzywdev:registry=https://npm.pkg.github.com
+@fuzefront:registry=https://npm.pkg.github.com
+@fuzeone:registry=https://npm.pkg.github.com
+
+# Auth comes from the environment — NEVER commit a token. In CI, GITHUB_TOKEN is
+# already present; locally, export a PAT with read:packages.
+//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
+always-auth=true
+
+# WHY THIS FILE ALSO LIVES AT THE REPO ROOT, not only next to the consuming
+# package.json: npm resolves .npmrc by walking UP from the current directory. A
+# repo whose only .npmrc sits in packages// gives no mapping at all to anything
+# run from the root — CI steps, tooling, a fresh clone — and every private scope
+# 404s there. FuzeBI had precisely that shape.
diff --git a/e2e/package.json b/e2e/package.json
index 115d074..3caa9ef 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -11,6 +11,10 @@
},
"devDependencies": {
"@playwright/test": "^1.47.0",
- "@types/node": "^20.0.0"
+ "@types/node": "^24.13.3"
+ },
+ "engines": {
+ "node": ">=24.0.0",
+ "npm": ">=10.0.0"
}
}
diff --git a/scripts/__tests__/test_gate_identifier.py b/scripts/__tests__/test_gate_identifier.py
new file mode 100644
index 0000000..0834062
--- /dev/null
+++ b/scripts/__tests__/test_gate_identifier.py
@@ -0,0 +1,208 @@
+"""Tests for scripts/gate_identifier.py.
+
+These deliberately assert that the gate **FAILS** on a violation, not only that
+it passes on a clean tree. A gate is only worth its runtime if it is known to
+fire; this repo has already been bitten once by a check that was green solely
+because its work was always done by someone else before it ran
+(`claude-auto-pr.yml` — every green run was the early-exit path, and the one
+time it actually had work to do it failed). Passing is not evidence.
+
+Run: python -m unittest discover -s scripts/__tests__ -p 'test_*.py'
+"""
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+import sys
+import tempfile
+import textwrap
+import unittest
+
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+GATE = os.path.join(REPO_ROOT, "scripts", "gate_identifier.py")
+
+
+def run_gate(root: str, *flags: str) -> subprocess.CompletedProcess:
+ return subprocess.run(
+ [sys.executable, GATE, root, *flags],
+ capture_output=True,
+ text=True,
+ timeout=180,
+ )
+
+
+class SyntheticRepo:
+ """A throwaway git repo — the gate walks `git ls-files`, so it needs one."""
+
+ def __init__(self) -> None:
+ self._tmp = tempfile.TemporaryDirectory()
+ self.root = self._tmp.name
+ subprocess.run(["git", "init", "-q", self.root], check=True)
+
+ def write(self, rel: str, content: str) -> None:
+ path = os.path.join(self.root, rel)
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(textwrap.dedent(content).lstrip("\n"))
+
+ def manifest(self, **fields) -> None:
+ self.write(".fuze/manifest.json", json.dumps(fields, indent=2))
+
+ def registry(self, **types: str) -> None:
+ body = "\n".join(f" {name}: '{prefix}'," for name, prefix in types.items())
+ self.write(
+ "packages/identity/src/registry.ts",
+ f"export const ENTITY_PREFIXES = {{\n{body}\n}} as const\n",
+ )
+
+ def commit(self) -> None:
+ subprocess.run(["git", "-C", self.root, "add", "-A"], check=True)
+ subprocess.run(
+ ["git", "-C", self.root, "-c", "user.email=t@t", "-c", "user.name=t",
+ "commit", "-qm", "fixture"],
+ check=True,
+ )
+
+ def __enter__(self) -> "SyntheticRepo":
+ return self
+
+ def __exit__(self, *exc) -> None:
+ self._tmp.cleanup()
+
+
+class TestNamespaceCheck(unittest.TestCase):
+ """--namespace: family-wide prefix uniqueness, enforced with no cross-repo state."""
+
+ def test_product_local_prefix_must_be_namespaced(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales", identity={"namespace": "sales"})
+ repo.registry(quote="quo")
+ repo.commit()
+ result = run_gate(repo.root, "--namespace")
+ self.assertEqual(result.returncode, 1, result.stdout)
+ self.assertIn("bare prefix 'quo'", result.stdout)
+ self.assertIn("sales_quo", result.stdout)
+
+ def test_namespaced_prefix_passes(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales", identity={"namespace": "sales"})
+ repo.registry(quote="sales_quo")
+ repo.commit()
+ result = run_gate(repo.root, "--namespace")
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+ def test_consuming_repo_may_not_mint_a_spine_prefix(self):
+ """The collision this whole tier exists to prevent: FuzeHub minting `usr`
+ would produce ids indistinguishable from FuzeFront's own users."""
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeHub", identity={"namespace": "hub"})
+ repo.registry(user="usr")
+ repo.commit()
+ result = run_gate(repo.root, "--namespace")
+ self.assertEqual(result.returncode, 1, result.stdout)
+ self.assertIn("owned by FuzeFront", result.stdout)
+
+ def test_owning_repo_may_mint_its_spine_prefix(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeFront", identity={"namespace": "front"})
+ repo.registry(user="usr", organization="org")
+ repo.commit()
+ result = run_gate(repo.root, "--namespace")
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+ def test_namespace_must_be_declared(self):
+ """Never derived from the directory name — a repo rename would silently
+ orphan every id already issued."""
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales")
+ repo.registry(quote="sales_quo")
+ repo.commit()
+ result = run_gate(repo.root, "--namespace")
+ self.assertEqual(result.returncode, 1, result.stdout)
+ self.assertIn("declares no 'identity.namespace'", result.stdout)
+
+ def test_malformed_namespace_is_rejected(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales", identity={"namespace": "Sales_X"})
+ repo.registry(quote="sales_quo")
+ repo.commit()
+ result = run_gate(repo.root, "--namespace")
+ self.assertEqual(result.returncode, 1, result.stdout)
+
+ def test_repo_with_no_registry_is_not_penalised(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales")
+ repo.write("src/app.ts", "export const x = 1\n")
+ repo.commit()
+ result = run_gate(repo.root, "--namespace")
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+
+class TestAdoptionCheck(unittest.TestCase):
+ """--adoption: the absence check the other families cannot make."""
+
+ MINTS = """
+ import { randomUUID } from 'node:crypto'
+ export function createQuote() {
+ const quoteId = randomUUID()
+ return { quoteId }
+ }
+ """
+
+ def test_minting_without_the_package_fails(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales")
+ repo.write("package.json", '{"name":"sales","dependencies":{"express":"^4.0.0"}}')
+ repo.write("src/quote.ts", self.MINTS)
+ repo.commit()
+ result = run_gate(repo.root, "--adoption")
+ self.assertEqual(result.returncode, 1, result.stdout)
+ self.assertIn("declares no dependency on an identity package", result.stdout)
+
+ def test_declaring_the_node_package_passes(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales")
+ repo.write(
+ "package.json",
+ '{"name":"sales","dependencies":{"@izzywdev/fuzefront-identity":"^0.1.0"}}',
+ )
+ repo.write("src/quote.ts", self.MINTS)
+ repo.commit()
+ result = run_gate(repo.root, "--adoption")
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+ def test_declaring_the_python_package_passes(self):
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales")
+ repo.write("package.json", '{"name":"sales"}')
+ repo.write("requirements.txt", "fuzefront-identity==0.1.0\n")
+ repo.write("src/quote.ts", self.MINTS)
+ repo.commit()
+ result = run_gate(repo.root, "--adoption")
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+ def test_repo_with_no_entity_work_is_not_penalised(self):
+ """A docs or infra repo owns no entities and must not be asked to adopt."""
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeDocs")
+ repo.write("package.json", '{"name":"docs"}')
+ repo.write("src/util.ts", "export const greet = () => 'hi'\n")
+ repo.commit()
+ result = run_gate(repo.root, "--adoption")
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+
+class TestFlagHandling(unittest.TestCase):
+ def test_unknown_flag_is_an_error_not_a_silent_pass(self):
+ """A typo'd flag used to run NOTHING and print OK — indistinguishable
+ from a clean tree, which is the worst possible failure for a gate."""
+ with SyntheticRepo() as repo:
+ repo.manifest(repo="izzywdev/FuzeSales")
+ repo.commit()
+ result = run_gate(repo.root, "--namespaces")
+ self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/scripts/build-frames-site.mjs b/scripts/build-frames-site.mjs
new file mode 100644
index 0000000..83d9a2d
--- /dev/null
+++ b/scripts/build-frames-site.mjs
@@ -0,0 +1,333 @@
+#!/usr/bin/env node
+/**
+ * build-frames-site.mjs — assemble the GitHub Pages site for design/frames/**.
+ *
+ * The top-level index is DERIVED from the directories actually present under
+ * design/frames/ (and each feature's own manifest.json). It is never a
+ * hand-maintained list: adding a feature directory is the only action needed for
+ * it to appear. A restated list is the two-sources-of-truth drift defect this
+ * repo has been bitten by repeatedly — do not reintroduce it here.
+ *
+ * Usage: node scripts/build-frames-site.mjs [--out ] [--frames ]
+ */
+import { readdir, readFile, mkdir, cp, writeFile } from 'node:fs/promises';
+import { existsSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+// fileURLToPath, not new URL().pathname — the latter yields "/D:/..." on Windows.
+const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+
+function arg(name, fallback) {
+ const i = process.argv.indexOf(name);
+ return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
+}
+
+const framesDir = path.resolve(arg('--frames', path.join(repoRoot, 'design', 'frames')));
+const outDir = path.resolve(arg('--out', path.join(repoRoot, '_site')));
+
+const escapeHtml = (s) =>
+ String(s ?? '').replace(/[&<>"']/g, (c) =>
+ ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]
+ );
+
+// Where approval decisions are recorded. A GitHub issue is the durable, auditable
+// record; the design-approval workflow (or a maintainer) reads it and flips the
+// per-flow `approved` in the manifest. Prefilled via plain query params — the only
+// form of issue prefill that is 100% reliable (dropdown prefill is not).
+// Repo-agnostic: derived from the Actions environment so this same script works in
+// any consuming repo. GITHUB_REPOSITORY is "owner/repo"; GitHub project Pages default
+// to https://.github.io/. Overridable via PAGES_BASE for custom domains;
+// falls back to placeholders for local runs outside CI.
+const REPO_SLUG = process.env.GITHUB_REPOSITORY || 'owner/repo';
+const [REPO_OWNER, REPO_NAME] = REPO_SLUG.split('/');
+const PAGES_BASE = process.env.PAGES_BASE || `https://${REPO_OWNER}.github.io/${REPO_NAME}`;
+const REPO_DEFAULT_BRANCH = process.env.GITHUB_DEFAULT_BRANCH || 'main';
+
+/** The per-flow list is the source of truth for approval: `build.flows`. (Some
+ * legacy manifests carried approvable entries under `frames`; fall back to that
+ * only if `build.flows` is absent, so the two never disagree silently.) */
+function flowsOf(manifest) {
+ const bf = manifest?.build?.flows;
+ if (Array.isArray(bf) && bf.length) return bf;
+ const legacy = Array.isArray(manifest?.frames)
+ ? manifest.frames.filter((f) => typeof f.approved === 'boolean')
+ : [];
+ return legacy;
+}
+
+/** New-issue URL that records a decision for one flow. Approve and Reject differ
+ * only in the prefilled `decision:` line, so the reviewer's click IS the record. */
+function approvalHref(slug, flow, decision, stamp) {
+ const flowId = flow.id ?? flow.orchestrator ?? 'flow';
+ const title = `design-approval: ${decision} ${slug} / ${flowId}`;
+ const body = [
+ '',
+ '',
+ '```yaml',
+ `feature: ${slug}`,
+ `flow: ${flowId}`,
+ `route: ${flow.route ?? ''}`,
+ `decision: ${decision}`,
+ `stamp: ${stamp ?? ''}`,
+ '```',
+ '',
+ `Frames: ${PAGES_BASE}/${slug}/`,
+ '',
+ decision === 'reject' ? '**Reason for rejection (required):**' : '_Optional note:_',
+ '',
+ ].join('\n');
+ const q = new URLSearchParams({ title, body, labels: 'design-approval' });
+ return `https://github.com/${REPO_SLUG}/issues/new?${q.toString()}`;
+}
+
+/** Fixed approval bar injected into every published frame of a feature, so the
+ * reviewer can approve/reject from wherever they are in the flow — not only the
+ * index. Derived entirely from the manifest's flow list. */
+function renderApprovalBar(slug, manifest) {
+ const flows = flowsOf(manifest);
+ // FULL stamp — the approve/reject links carry this into the design-approval
+ // issue, where design-approval.yml compares it EXACTLY against the recomputed
+ // 64-char stamp. Truncating here made every click fail as "frames changed".
+ // (Display elsewhere may truncate for readability; the link must not.)
+ const stamp = manifest?.stamp ? String(manifest.stamp) : '';
+ if (!flows.length) {
+ return `
No flows declared in manifest.build.flows — nothing to approve.
+
+ Review · ${escapeHtml(slug)} approve or reject each flow ↓
+
${rows}
+
+
+`;
+}
+
+/** Inject the approval bar into an HTML document just before
+
+
Contract-freeze · UX approval
+
${REPO_NAME} — Design Frames
+
Every feature awaiting UX approval. Each entry is a navigable HTML frame set built in
+ the fuse-seam design system — the visual contract that implementation and Playwright verification
+ are measured against. This directory is generated from the folders present in
+ design/frames/; it is never hand-maintained.
+
+
+${features.length ? cards : empty}
+
+
+
+
+
(or append
+ * if none). Idempotent-ish: skips if already present. */
+function injectApprovalBar(html, bar) {
+ if (html.includes('data-ff-approve')) return html;
+ if (html.includes('')) return html.replace('', `${bar}\n`);
+ return html + bar;
+}
+
+/** Feature directories present on disk — the source of truth. `_`-prefixed dirs
+ * (e.g. `_template`) are scaffolding, not features awaiting review. */
+async function discoverFeatures() {
+ const entries = await readdir(framesDir, { withFileTypes: true });
+ const features = [];
+ for (const e of entries) {
+ if (!e.isDirectory() || e.name.startsWith('_') || e.name.startsWith('.')) continue;
+ const dir = path.join(framesDir, e.name);
+ const manifestPath = path.join(dir, 'manifest.json');
+ let manifest = null;
+ let manifestError = null;
+ if (existsSync(manifestPath)) {
+ try {
+ manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
+ } catch (err) {
+ manifestError = err.message;
+ }
+ }
+ const entryFile = manifest?.entry ?? 'index.html';
+ features.push({
+ slug: e.name,
+ manifest,
+ manifestError,
+ hasEntry: existsSync(path.join(dir, entryFile)),
+ entryFile,
+ });
+ }
+ return features.sort((a, b) => a.slug.localeCompare(b.slug));
+}
+
+/** Approval state derived from the manifest: per-flow `approved` (the current
+ * model) or the legacy top-level `approved`. Reported, never asserted. */
+function approvalOf(manifest) {
+ if (!manifest) return { label: 'no manifest', state: 'unknown' };
+ // Per-flow approval lives in build.flows (the source of truth); flowsOf falls
+ // back to the legacy `frames` shape so old and new manifests read consistently.
+ const perFlow = flowsOf(manifest).filter((f) => typeof f.approved === 'boolean');
+ if (perFlow.length > 0) {
+ const yes = perFlow.filter((f) => f.approved).length;
+ if (yes === perFlow.length) return { label: `approved (${yes}/${perFlow.length} flows)`, state: 'approved' };
+ if (yes === 0) return { label: `pending (0/${perFlow.length} flows)`, state: 'pending' };
+ return { label: `partial (${yes}/${perFlow.length} flows)`, state: 'partial' };
+ }
+ if (manifest.approved === true) return { label: 'approved', state: 'approved' };
+ return { label: 'pending', state: 'pending' };
+}
+
+function renderIndex(features) {
+ const cards = features
+ .map((f) => {
+ const m = f.manifest;
+ const title = escapeHtml(m?.feature ?? m?.name ?? f.slug);
+ const desc = escapeHtml(m?.description ?? 'No description in manifest.json.');
+ const approval = approvalOf(m);
+ const frameCount = Array.isArray(m?.frames) ? m.frames.length : 0;
+ const stamp = m?.stamp ? `${escapeHtml(String(m.stamp).slice(0, 12))}` : 'unstamped';
+ const warn = f.manifestError
+ ? `
manifest.json failed to parse: ${escapeHtml(f.manifestError)}