Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/agents/backend-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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.
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/contract-designer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `@<scope>/<svc>-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.
Expand Down
2 changes: 2 additions & 0 deletions .claude/agents/test-engineer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
20 changes: 19 additions & 1 deletion agent-templates/sync/role_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
60 changes: 51 additions & 9 deletions deploy/argocd/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,62 @@
# Argo CD wiring for FuzeAgent — a FuzeInfra delegation
# Argo CD wiring for FuzeAgent — owned by this repo

**FuzeInfra owns Argo `Application` and `AppProject` resources.** A product repo
does not author them; two repos independently declaring an Application for the
same workload is the competing-unrestricted-app failure FuzeMarket PR #61
removed. This file is the **handoff spec**, not a manifest.
`deploy/argocd/` holds FuzeAgent's app-of-apps and its three child Applications,
and this repo owns them. FuzeInfra registers this directory once; ArgoCD
self-syncs it thereafter.

> **The manifests already in this directory are live wiring. This change
> neither adds, edits nor removes any of them.**
## Who owns what — correcting an earlier claim in this file

| Existing manifest | Deploys |
An earlier revision opened *"FuzeInfra owns Argo `Application` and `AppProject`
resources. A product repo does not author them ... This file is the handoff spec,
not a manifest."* **That is wrong**, and this directory already contradicted it —
it has carried four Application manifests the whole time, and FuzeInfra's
`argocd/applications/fuzeagent.yaml` is a *root* app-of-apps that points **at**
them, i.e. it is built on the assumption that this repo authors them.

FuzeInfra's own onboarding contract
([`docs/CONSUMER_ONBOARDING_SHARED_CLUSTER.md`](https://github.com/izzywdev/FuzeInfra/blob/main/docs/CONSUMER_ONBOARDING_SHARED_CLUSTER.md)):

> Boundary: the consumer owns its `deploy/**` (Helm/kustomize + Argo Applications
> + sealed secrets). FuzeInfra owns the cluster, Argo, the tunnel, and the shared
> datastores.

FuzeInfra's `argocd-register` workflow is built around it: it takes a *"Path in
the consumer repo holding the Argo Application/AppProject manifests"*,
`kubectl apply`s the directory **once**, and then

> After the first registration, ArgoCD polls and self-syncs the consumer's
> `deploy/argocd` manifests.

Registration is a **one-time owner action** — never `kubectl apply` from CI or by
hand from here.

That mistaken claim was propagated to several sibling repos in the same session
and used to delete their Applications outright (FuzeService #32, FuzeSales #44),
leaving them with no path to prod. Those deletions are being reverted. **None of
FuzeAgent's manifests were deleted**, so nothing here changes but the wording.

## The invariant that IS real

**One Application per workload.** FuzeContact #33 and FuzeMarket #61 removed
*duplicates*: two Applications with `prune: true` + `selfHeal: true` on the same
namespace with disagreeing values, each pruning what the other did not create.

FuzeAgent's manifests are **not** that — the app-of-apps recurses into
`applications/`, and each child owns a **different** chart:

| Manifest | Deploys |
|---|---|
| `app-of-apps.yaml` | recursive discovery of `applications/` |
| `applications/fuzeagent.yaml` | `deploy/helm/fuzeagent` — the product chart this change extends |
| `applications/fuzeagent.yaml` | `deploy/helm/fuzeagent` — the product chart |
| `applications/fuzeagent-sealed.yaml` | the sealed-secret bundle |
| `applications/a2a-shared.yaml` | `deploy/helm/a2a-shared` — **the family's only A2A server** |

All four use `project: fuzeagent`, the restricted AppProject FuzeInfra owns at
`argocd/projects/fuzeagent.yaml`. That project **does** exist — verified.

> **These four manifests are live wiring. Do not delete them, and do not add a
> second Application for any chart already listed above.**

## The A2A finding — read this first

**`a2a-shared` is wired for deployment, not merely implemented.** This was the
Expand Down