docs(spec): add C2 integration section (sprint 8 commit #1)

Introduce the SPEC section for the Mythic C2 integration layer.
Covers RBAC (RT-only, SOC=403), per-engagement Fernet-encrypted config,
c2_config + c2_task data model with ON DELETE CASCADE, full endpoint
list, output mapping rules (append-only, idempotent), 2500 ms polling
and the fake/real adapter selection via MIMIC_C2_ADAPTER.

Also patch tasks/todo.md: fix pytest baseline (256 from main, not 253),
make cascade-delete explicit, pin the MythicMeta/Mythic_Scripting source
version and document defensive base64 handling.

Closes spec-reviewer WARN-1 (SPEC ↔ plan parity), WARN-2 (cascade),
INFO-1 (pinned source), INFO-3 (baseline).
This commit is contained in:
Knacky
2026-06-10 19:07:35 +02:00
parent 6ca614a3f3
commit 813e69ee01
2 changed files with 137 additions and 133 deletions

66
SPEC.md
View File

@@ -59,8 +59,70 @@ CSV : exactement 1 ligne d'en-tête + 1 ligne par simulation. Markdown : en-têt
Prévoir un module d'authentification : dans un premier temps local à la bdd. Prévoir un module d'authentification : dans un premier temps local à la bdd.
Dans un premier temps, il s'agit juste de notifier manuellement de l'exécution et les résultats des tests. Dans un premier temps, il s'agit juste de notifier manuellement de l'exécution et les résultats des tests.
Dans un second temps, après que la V1 soit terminée, nous ajouterons une couche permettant de se connecter à un C2 (mythic ou maison) afin d'exécuter des simulation au travers du C2. Dans un second temps, après que la V1 soit terminée, nous ajouterons une couche permettant de se connecter à un C2 (mythic ou maison) afin d'exécuter des simulation au travers du C2.
## Intégration C2 (Sprint 8+)
Couche d'intégration C2 permettant d'exécuter les commandes d'une simulation à travers un Command & Control distant, suivre l'avancement des tâches en quasi-temps réel, et importer l'historique d'exécutions existant. **Implémentation de référence : Mythic 3.x**, derrière une interface `C2Adapter` mince qui ne ferme pas la porte à un C2 maison ultérieur.
**RBAC C2 = ressource Red Team uniquement** (précédent Templates + Export) : admin et redteam ont accès complet (config + exécution + import). SOC retourne 403 sur tous les endpoints C2 (pas de nav link, pas d'affichage du panneau C2).
**Configuration par engagement** : chaque engagement possède au plus une `c2_config` (URL Mythic + API token + flag `verify_tls`). Le token est **chiffré au repos** via `cryptography.Fernet` ; la clé est dérivée de l'env var `MIMIC_ENCRYPTION_KEY` (variable obligatoire pour activer la fonctionnalité C2 — jamais hardcodée, conforme à la règle OPSEC zero-secret-in-code). Le token n'est jamais renvoyé en clair par l'API — `GET /api/engagements/<id>/c2-config` retourne `has_token: bool` uniquement. Mise à jour via `PUT` ; suppression via `DELETE`. La suppression d'un engagement supprime en cascade sa `c2_config`.
**Sélection d'adapter** via l'env var `MIMIC_C2_ADAPTER` :
- `mythic` (défaut) : adapter Mythic réel (GraphQL via Hasura).
- `fake` : adapter en mémoire déterministe utilisé pour la validation Playwright et le dev local sans instance Mythic.
**Modèle de données — additions** :
`c2_config` (1 ligne par engagement au max) :
| Colonne | Type | Notes |
|---|---|---|
| `id` | int PK | |
| `engagement_id` | int FK `engagements.id` ON DELETE CASCADE, **UNIQUE** | |
| `url` | text | endpoint Mythic, ex. `https://lab.internal:7443` |
| `api_token_encrypted` | text | Fernet ciphertext, jamais en clair |
| `verify_tls` | bool, défaut `true` | `false` autorisé pour labs auto-signés |
| `created_at`, `updated_at` | datetime | |
`c2_task` (lien simulation ↔ tâche Mythic) :
| Colonne | Type | Notes |
|---|---|---|
| `id` | int PK | |
| `simulation_id` | int FK `simulations.id` ON DELETE CASCADE | |
| `mythic_task_display_id` | int | identifiant côté Mythic |
| `callback_display_id` | int | callback Mythic sur lequel la tâche tourne |
| `command` | text | commande envoyée |
| `params` | text nullable | paramètres associés |
| `status` | text | statut brut Mythic (`submitted`, `completed`, `error`, …) |
| `completed` | bool | `true` quand la tâche est terminée |
| `output` | text nullable | sortie décodée (base64 → utf-8 ; binaire → préfixe `<binary>` + hex) |
| `source` | enum `mimic` \| `import` | tâche lancée depuis Mimic ou importée a posteriori |
| `created_at` | datetime | |
| `completed_at` | datetime nullable | timestamp de complétion |
**Endpoints C2** (tous admin+redteam ; SOC = 403) :
- `GET /api/engagements/<id>/c2-config``{has_token, url, verify_tls}` (jamais le token en clair).
- `PUT /api/engagements/<id>/c2-config``{url, api_token?, verify_tls}`.
- `DELETE /api/engagements/<id>/c2-config`.
- `POST /api/engagements/<id>/c2-config/test` — test de connectivité via l'adapter, renvoie `{ok, error?}`.
- `GET /api/engagements/<id>/c2/callbacks` — callbacks actifs de l'instance Mythic configurée.
- `POST /api/simulations/<id>/c2/execute` `{callback_display_id, commands: [str]}` — une tâche Mythic par commande, stockées dans `c2_task` (source=`mimic`). **Auto-transition** : si la simulation est `pending`, elle passe à `in_progress` (même règle que l'édition manuelle RT — cf. § Fonctionnement).
- `GET /api/simulations/<id>/c2/tasks` — poll-on-read : à la lecture, rafraîchit le statut et l'output des `c2_task` non terminées depuis Mythic, applique le mapping de sortie (voir ci-dessous) à la simulation pour chaque tâche qui vient de se terminer (idempotent — appliqué une seule fois par tâche).
- `GET /api/engagements/<id>/c2/callbacks/<cid>/history?page=` — historique paginé des tâches d'un callback, pour l'import.
- `POST /api/simulations/<id>/c2/import` `{task_display_ids: [int]}` — import sélectif de tâches (source=`import`) + mapping de sortie.
**Mapping de sortie vers la simulation** (appliqué une fois par tâche, lors de la complétion ou de l'import) :
- `simulation.execution_result` reçoit en append le bloc `\n$ <command>\n<output>\n` (préserve l'existant, jamais d'écrasement).
- `simulation.executed_at` est renseigné depuis le timestamp de la première tâche complétée si le champ est vide ; sinon non modifié.
- `simulation.commands` reçoit en append la commande si elle n'y figure pas déjà (déduplication ligne par ligne).
**Suivi temps réel** : polling court — le frontend re-fetch `GET /api/simulations/<id>/c2/tasks` toutes les **2 500 ms** via TanStack Query `refetchInterval` tant qu'une tâche attachée n'est pas terminée ; le polling s'arrête automatiquement quand toutes les tâches sont `completed`. Pas d'infrastructure ajoutée côté serveur (pas de WebSocket, pas de scheduler).
**UI** : les contrôles C2 vivent dans la carte Red Team de l'écran simulation — bouton `[Execute via C2]` ouvrant une modale (picker de callback + textarea de commandes pré-remplie depuis `commands`), panneau des tâches attachées sous la carte, et modale d'import historique. Configuration C2 visible/éditable depuis l'écran de détail/édition d'engagement.
**Validation** : MVP entièrement mocké — pytest utilise un adapter mocké (zéro HTTP live), Playwright utilise l'adapter `fake` (déterministe). Le branchement contre une instance Mythic réelle est repoussé au premier usage opérationnel et peut nécessiter un patch mineur du contrat GraphQL.
## Stacks techniques ## Stacks techniques
* **FrontEnd** : WebUI * **FrontEnd** : WebUI
- Stacks standard : ReactJS, Vite, TailWind etc... - Stacks standard : ReactJS, Vite, TailWind etc...

View File

@@ -1,153 +1,95 @@
# Sprint 7Design Refresh: Terminal-SOC Aesthetic # Sprint 8C2 Layer: Mythic Integration
> Branch : `sprint/7-design` · Worktree : `.claude/worktrees/sprint-7-design` · Base : `main` @ `e27babe` > Branch : `sprint/8-c2` · Worktree : `.claude/worktrees/sprint-8-c2` · Base : `main` @ `6ca614a`
## §0 — Binding decisions (locked with user, 2026-06-09) SPEC.md phase 2: "après que la V1 soit terminée, nous ajouterons une couche permettant
de se connecter à un C2 (mythic ou maison) afin d'exécuter des simulations au travers du C2."
1. **Visual direction**: Bloomberg / Terminal-SOC — dense, brutalist, semantic colors strong, no ornament. ## §0 — Binding decisions (locked with user, 2026-06-10)
2. **Border-radius**: **0 everywhere** except status pills (`rounded-pill`) and avatars (round). All buttons, cards, modals, inputs, dropdowns, tables, tags → angular.
3. **Palette**: KEEP current (`#024ad8` primary blue, `slab #111827`, `canvas/paper/cloud/fog/ink` light+dark vars). ADD semantic tokens `success-green` + `warn-amber` (confirmed scope add — needed for SOC-grade status legibility on dashboards and badges).
4. **Scope**: Refonte globale en 1 sprint (all 8 pages + 17 components + tokens + DESIGN.md).
5. **Monospace**: data-only — JetBrains Mono for IDs, dates ISO, commands, execution output, MITRE techniques, metrics. Inter stays for body/labels/headers.
6. **Mono font**: JetBrains Mono, bundled locally via `@fontsource-variable/jetbrains-mono` (consistent with existing Inter bundle).
7. **Modes**: KEEP light + dark both. Toggle stays.
8. **Animations**: **Brutalist — zero transition**. Remove all `transition-*` utilities, focus rings sharp, hover instantaneous.
9. **Display scale reduction**: locked. `display-xxl 72→40`, `display-xl 56→32`, `display-lg 44→28`, `display-md 32→24`, `display-sm 24→20`, `display-xs 20→16`. Headers stay modest in terminal aesthetic — no editorial flourish at hero scale.
## §1 — Pre-work checks (team-lead, before dispatch) 1. **C2 target**: Mythic 3.x, behind a thin `C2Adapter` interface (keeps the door open for a custom C2 later).
2. **Scope**: FULL bidirectional — execute + near-real-time tracking + history import. User explicitly overrode the "one-shot first" recommendation; overrun risk accepted (see R1).
3. **C2 config**: per-engagement (URL + API token). Token is write-only at the API level — never returned in clear.
4. **UI anchor**: execution lives in the simulation screen (Red Team card).
5. **Realtime mechanism**: short polling. Frontend: TanStack Query `refetchInterval` 2 500 ms while any attached task is incomplete. Backend: poll-on-read — refreshes non-completed tasks from Mythic when the task list is read. No scheduler, no new infra.
6. **Secret storage**: API token Fernet-encrypted in SQLite. Key from env var `MIMIC_ENCRYPTION_KEY` (mandatory to enable C2 features, never hardcoded — OPSEC rule).
7. **History import**: BOTH — auto-attach of tasks launched from Mimic AND manual browse/select of the callback's task history.
8. **Validation**: fully mocked — no dev Mythic instance available. pytest uses a mocked adapter; e2e uses a built-in `FakeAdapter` selected via `MIMIC_C2_ADAPTER=fake`.
9. **RBAC**: C2 is a RedTeam resource — admin + redteam full access, SOC gets 403 on every C2 endpoint (same precedent as Templates and Export).
10. **Workflow tie-in**: launching a C2 execution auto-transitions the simulation `pending → in_progress` (same rule as a manual RT edit).
11. **Output mapping**: when a task completes (or is imported), its output is APPENDED to `execution_result` prefixed by a `$ <command>` header line; `executed_at` is set from the first task's timestamp if empty; the command is appended to `commands` if not already present.
- [ ] Confirm `tasks/lessons.md` has nothing contradicting this brief ## §1 — Mythic adapter contract (pinned from MythicMeta/Mythic_Scripting client source)
- [ ] Verify uncommitted `.claude/agents/frontend-builder.md` patch (Skill mandatory) is restored in worktree — sprint hygiene
- [ ] Send plan to **spec-reviewer** for 2-pass approval (vs SPEC.md, vs §0 binding decisions). MUST be APPROVED before any code touches `frontend/`.
- [ ] After approval: dispatch frontend-builder with this todo as brief.
## §2 — Sprint hygiene (commit #1) - Transport: POST `https://<host>:7443/graphql`, header `apitoken: <token>` (Hasura behind nginx).
- Issue task: mutation `createTask(callback_id: <display_id>, command, params, tasking_location: "command_line")`.
- List callbacks: query on `callback` table — `id, display_id, active, host, user, domain, last_checkin`.
- Task status: query on `task` table — `display_id, status, completed` (client lib uses a subscription; we poll the query instead, per decision 5).
- Task output: query on `response` table ordered by id — `response_text` is **base64-encoded**, decode + concatenate.
- History: query `task` filtered by `callback_display_id`, paginated, with command + status + timestamps.
- [ ] `chore(agents): frontend-builder must invoke Skill frontend-design before UI work` — lands BEFORE design work so the agent itself triggers the Skill on first call this sprint. `C2Adapter` interface (`backend/app/services/c2/adapter.py`):
- `test_connection() -> C2Health`
- `list_callbacks() -> list[C2Callback]`
- `create_task(callback_display_id, command, params) -> int` (task display_id)
- `get_task(task_display_id) -> C2TaskStatus`
- `get_task_output(task_display_id) -> str` (decoded)
- `list_callback_tasks(callback_display_id, page, page_size) -> C2TaskPage`
## §3 — Foundation: DESIGN.md + tokens (commits #2#4) Implementations: `MythicAdapter` (requests, `verify_tls` flag from config — lab instances run self-signed), `FakeAdapter` (deterministic in-memory data, selected by `MIMIC_C2_ADAPTER=fake`, also powers e2e).
### §3.1 DESIGN.md rewrite (commit #2) ## §2 — Backend (backend-builder) — milestones M1→M4
- [ ] Replace current HP-catalog doc (346 lines, off-brand) with terminal-SOC spec covering: **Data model (1 Alembic migration):**
- **Overview**: brutalist BAS Purple Team console, angular surfaces, semantic color signals, data-monospace hybrid - `c2_config`: id, engagement_id FK **unique** (`ON DELETE CASCADE`, same precedent as `simulations.engagement_id`), url, api_token_encrypted, verify_tls bool (default true), created_at, updated_at.
- **Colors**: keep all existing tokens with **role redefinition** for terminal-SOC context. Primary = neutral action. Bloom-deep/coral = destructive/alert. ADD `success` (green) + `warn` (amber) — locked §0 D3 — with light + dark variants and WCAG AA contrast on slab and canvas surfaces - `c2_task`: id, simulation_id FK (`ON DELETE CASCADE`), mythic_task_display_id int, callback_display_id int, command text, params text nullable, status text, completed bool, output text nullable, source enum(`mimic`,`import`), created_at, completed_at nullable.
- **Typography**: Inter (body/headers/labels) + JetBrains Mono (data). Concrete tier table with size/weight/line-height
- **Layout**: tighter spacing (replace `section 80px``section 48px`; halve card padding on dense surfaces)
- **Shapes**: ALL radii = 0 except `rounded-pill` reserved for status badges and avatars
- **Components**: re-document `btn-*`, `text-input`, `card-*`, `badge-*`, `nav-*`, `modal-*` with brutalist specs (no shadow, hairline borders, zero transition)
- **Do's/Don'ts**: zero rounded on conteneurs; zero transitions; semantic colors only on status surfaces; mono ONLY for data, never headers
- **Iteration guide**
- [ ] Doc lives in English (in-repo).
### §3.2 Tailwind token refresh (commit #3) **Endpoints (all admin+redteam; SOC → 403):**
- `GET/PUT/DELETE /api/engagements/<id>/c2-config` — GET returns `has_token: bool`, never the token.
- `POST /api/engagements/<id>/c2-config/test` — connectivity check via adapter.
- `GET /api/engagements/<id>/c2/callbacks` — active callbacks.
- `POST /api/simulations/<id>/c2/execute` `{callback_display_id, commands: [str]}` — one Mythic task per command, rows in `c2_task` (source=mimic), auto-transition pending→in_progress.
- `GET /api/simulations/<id>/c2/tasks` — poll-on-read: refresh incomplete tasks from Mythic; on completion fetch output and apply decision 11 mapping (idempotent, applied once).
- `GET /api/engagements/<id>/c2/callbacks/<cid>/history?page=` — paginated callback history for import.
- `POST /api/simulations/<id>/c2/import` `{task_display_ids: [int]}` — import selected tasks (source=import) + decision 11 mapping.
- [ ] `frontend/tailwind.config.ts`: **Milestones:** M1 crypto service (Fernet) + migration + config CRUD + test endpoint → M2 callbacks + execute → M3 poll-on-read status/output + mapping → M4 history + import.
- `borderRadius`: keep `none: 0`, keep `pill: 9999px`. Drop or stop using `xs/sm/md/lg/xl` for surfaces — keep only if a badge variant needs `2px` **Tests:** pytest with mocked adapter (~35-45 new), zero live HTTP. Crypto round-trip, RBAC 403 matrix, mapping idempotence, migration up/down.
- ADD `fontFamily.mono`: `['JetBrains Mono Variable', 'JetBrains Mono', 'ui-monospace', 'monospace']`
- ADD semantic colors (locked §0): `success: { DEFAULT, soft }` (green) + `warn: { DEFAULT, soft }` (amber). Pull dark-mode variants from CSS vars too. Suggested anchors — `success #16a34a` (dark `#22c55e`), `warn #d97706` (dark `#f59e0b`); design-reviewer audits WCAG AA at both modes.
- Reduce `display-*` scale (locked §0): `display-xxl 72px → 40px`, `display-xl 56→32`, `display-lg 44→28`, `display-md 32→24`, `display-sm 24→20`, `display-xs 20→16` — terminal headers are modest
- Drop `tracking[0.7px]` and uppercase from `button-md` (still ALLCAPS via class but no letter-spacing)
- Drop shadow tokens or keep but ensure no component class applies them
- [ ] `frontend/src/styles/index.css`:
- Drop `font-size: 16.5px` root bump (back to `16px` standard)
- Set body `line-height: 1.4`, tighten headings to 1.1
- Rewrite `.btn-primary/ink/outline/outline-ink`: `rounded-none`, NO `transition-colors`, keep `uppercase`, drop `tracking-[0.7px]`, keep `h-11` (touch target)
- Rewrite `.text-input`: `rounded-none`, focus border-primary sharp (no halo), no transition
- Rewrite `.card-product` and any `.card-*`: `rounded-none`, no shadow, 1px hairline border for separation
- Rewrite `.badge-pill-*`: keep `rounded-pill` ONLY here (status badges); strip uppercase if applied
- Rewrite `.modal-backdrop`: same dark backdrop, no rounded for the modal frame itself
- ADD `.mono` utility or rely on Tailwind's `font-mono` (preferred) for data cells
### §3.3 JetBrains Mono bundle (commit #4) ## §3 — Frontend (frontend-builder)
- [ ] `cd frontend && npm i @fontsource-variable/jetbrains-mono` - **Engagement detail/form**: "C2 configuration" card — URL, token (password input, write-only placeholder when `has_token`), verify-TLS checkbox, [Test connection] with result feedback. Admin+redteam only.
- [ ] `frontend/src/styles/fonts.css`: add `@import '@fontsource-variable/jetbrains-mono'` - **SimulationFormPage RT card**: [Execute via C2] button (hidden when no config; disabled when done) → modal: callback picker table (display_id, host, user, active, last_checkin — font-mono data cells) → commands textarea prefilled from `rt.commands` → Launch.
- [ ] No CDN. Confirms via `npm ls @fontsource-variable/jetbrains-mono`. - **C2 tasks panel** (under RT card): table of attached tasks — command (mono), status badge (semantic tokens), completed_at (mono). `refetchInterval` 2 500 ms while any incomplete; stops when all done.
- **Import history modal**: callback picker → paginated task list with checkboxes → [Import selected].
- Terminal-SOC compliance: rounded-none, zero transitions, hairline borders, mono for data only. Status colors via `success`/`warn` semantic tokens.
- Vitest ~15-20 new tests. `data-testid` on every new interactive surface for e2e.
## §4 — Component sweep (commit #5) ## §4 — Docs & hygiene
Rule: `rounded-*``rounded-none` unless explicitly an avatar or status pill; remove `transition-*`; data text → `font-mono`. - **SPEC.md C2 section = FIRST commit** of the sprint (sprint 5/6 lesson: never close a sprint with SPEC uncommitted).
- README: `MIMIC_ENCRYPTION_KEY`, `MIMIC_C2_ADAPTER` env vars + docker compose example.
- CHANGELOG sprint 8 entry at close.
- [ ] `Layout.tsx`: nav-bar/utility-strip already angular — confirm. Remove `transition-colors` on theme button and hover-underline transitions. Mono font for any data label exposed (e.g. user.role pill). ## §5 — Pipeline & sequencing
- [ ] `StatusBadge.tsx`: KEEP rounded → switch to `rounded-pill` (it's a status pill, locked exception). Audit semantic mapping (planned/active/closed → semantic tokens once added).
- [ ] `SimulationStatusBadge.tsx`: same — `rounded-pill`, semantic colors aligned with new tokens.
- [ ] `FormField.tsx`: angular inputs (already via `.text-input` recipe — confirm).
- [ ] `EmptyState.tsx`: angular wrapper. No rounded illustration container.
- [ ] `ErrorState.tsx`: angular. Bloom-deep border-left if signalling.
- [ ] `LoadingState.tsx`: drop any rounded spinner background. Spinner shape ok.
- [ ] `ConfirmDialog.tsx`: angular modal. Buttons via new `.btn-*` recipes.
- [ ] `Toast.tsx`: angular. Semantic color border-left strip.
- [ ] `ExportEngagementButton.tsx` (sprint 6): angular dropdown menu. Audit `rounded-*` in the menu/item classes.
- [ ] `MitreMatrixModal.tsx`: angular modal. Cells already grid — confirm no rounded.
- [ ] `MitreTechniquePicker.tsx`: angular dropdown.
- [ ] `MitreTechniquesField.tsx`: angular chips.
- [ ] `MitreTechniqueTag.tsx`: angular tag (NOT pill — terminal tag, not a status). Decide once and apply consistently across MITRE surfaces.
- [ ] `TemplatePickerModal.tsx`: angular modal.
- [ ] `SimulationList.tsx`: angular table. Data cells (commands, executed_at, MITRE techniques) → `font-mono`.
- [ ] `ProtectedRoute.tsx`: no visual surface, skip.
## §5 — Page sweep (commit #6) 1. spec-reviewer pre-pass on this plan (before any code).
2. backend-builder M1+M2 → API contract frozen → frontend-builder starts (parallel with backend M3+M4).
3. design-reviewer on new UI surfaces (screenshots come from the Playwright run — e2e bootstraps its own users, which sidesteps the sprint-7 credentials wall).
4. **code-reviewer must be respawned** (killed 2026-06-10 after idle-loop malfunction) before the review phase.
5. test-verifier: new C2 Playwright specs (fake adapter) + **full 223-spec re-run** — also clears the sprint 7 e2e debt (suite never re-ran after the design refresh).
For each page: header/body/footer review, replace rounded card containers with angular hairline-bordered containers, ensure data cells use mono. ## §6 — Risks
- [ ] `LoginPage.tsx`: angular form card. Drop ornament. - **R1 — scope**: full bidirectional in one sprint. Mitigation: M1→M4 are ordered; M4 (history import) is the cut line if we overrun. User accepted explicitly.
- [ ] `EngagementsListPage.tsx`: angular table container (currently `.card-product` with rounded-xl). Data cells (dates) → mono. - **R2 — schema fidelity**: no live Mythic to validate against; adapter pinned to the official `MythicMeta/Mythic_Scripting` client source on **master @ 2026-06-10** (verified via raw.githubusercontent.com). First real connection may need a small patch. README records the exact source URL alongside the "may need a patch" note. Adapter must defensively handle `response_text` base64 decode failures (binary output): on `binascii.Error` keep raw bytes hex-encoded with a prefix `<binary> ` so `execution_result` never silently corrupts.
- [ ] `EngagementDetailPage.tsx`: angular header section. Engagement metadata (start/end dates, IDs, created_at) in mono. Simulations table covered via SimulationList. - **R3 — secret at rest**: Fernet + env key; key rotation out of scope (documented limitation).
- [ ] `EngagementFormPage.tsx`: angular form. Date inputs ok. - **R4 — polling load**: poll-on-read touches only incomplete tasks of the open simulation — bounded.
- [ ] `SimulationFormPage.tsx`: angular form. Commands textarea → mono. - **R5 — e2e drift**: sprint 7 redesign never re-validated by Playwright; the full re-run in §5.5 surfaces any breakage — budget fix time.
- [ ] `TemplatesListPage.tsx`: angular list.
- [ ] `TemplateFormPage.tsx`: angular form. Commands field → mono.
- [ ] `UsersAdminPage.tsx`: angular table. Username column → mono (it's an ID).
## §6Test refresh (commit #7) ## §7Definition of Done
- [ ] `cd frontend && npm run test -- --run` — identify failing assertions on class names (`rounded-xl`, `card-product`, etc.). Update tests to use semantic queries (role, name, data-testid) where possible; if test asserts on visual class, update assertion to the new class. - pytest green (256 baseline on main + new), vitest green (139 baseline + new), Playwright full suite green (223 baseline + new C2 specs).
- [ ] No new vitest tests added (visual sprint, behavior unchanged). - spec-reviewer (plan) + design-reviewer (UI) + code-reviewer (diff) all APPROVED.
- [ ] Playwright e2e: should be `data-testid`-driven — run full suite to confirm no regression. If breakage, fix the testid not the test logic. - SPEC.md, README.md, CHANGELOG.md updated. No secret, key or IP hardcoded anywhere.
- PR #11 opened via `make open-pr`.
## §7 — Reviews
- [ ] **spec-reviewer** (pre-dispatch, §1): plan validated vs SPEC.md and §0 binding decisions
- [ ] **frontend-builder** (§2-§6): implements, runs typecheck/lint/test, delivers screenshots for design-reviewer (every page + key states, light+dark)
- [ ] **design-reviewer** (post-frontend): reviews screenshots + diff vs new DESIGN.md. Brutalist consistency, mono-discipline (only data), zero-rounded discipline.
- [ ] **code-reviewer** (post-design): reviews frontend diff for duplication, lost reuse, dead code.
- [ ] **test-verifier**: skipped this sprint (no new US, no behavior change).
- [ ] **backend-builder**: idle, no work this sprint.
## §8 — Git workflow
- [ ] Branch: `sprint/7-design` (already created from origin/main)
- [ ] Commits: conventional, one per logical group (§2 to §7)
- [ ] PR via `make open-pr` (Gitea pattern, per memory)
- [ ] PR body in `tasks/pr-body-sprint-7.md`
- [ ] CHANGELOG.md sprint 7 section
## §9 — Risks & mitigations
- **R1 — Tests break en masse**: many vitest specs may assert on class strings (e.g., `rounded-xl` on cards). Mitigation: update assertions to semantic queries; budget half a phase to test repair.
- **R2 — Dark mode contrast lost**: angular + new semantic colors may break WCAG AA contrast on dark slab. Mitigation: design-reviewer audits both modes; adjust the dark variant hex to meet WCAG AA. Rollback the success/warn family only if no accessible green/amber is achievable on the dark slab.
- **R3 — Mono overflow**: JetBrains Mono is wider than Inter at same px. Cell widths in tables may overflow. Mitigation: keep `table-layout: fixed` and `word-break: break-word` (pattern reused from PDF export CSS sprint 6).
- **R4 — DESIGN.md rewrite churn**: replacing 346 lines is a big diff. Mitigation: rewrite atomically in commit #2, keep token names consistent so downstream commits don't drift.
- **R5 — User taste mismatch**: "Bloomberg/SOC" may not match user's mental image. Mitigation: design-reviewer screenshots → user check-in BEFORE merge.
## §10 — Definition of Done
- [ ] All §0 decisions reflected in DESIGN.md + tokens + components + pages
- [ ] `npm run typecheck` clean
- [ ] `npm run lint` clean
- [ ] `npm run test -- --run` all green
- [ ] Backend untouched — `git diff origin/main -- backend/` empty
- [ ] Playwright e2e green (223 baseline preserved)
- [ ] Screenshots delivered (light + dark) for every page + key states
- [ ] DESIGN.md rewritten, no HP/Forma/wordmark/chevron references
- [ ] CHANGELOG.md sprint 7 section
- [ ] PR opened on Gitea
- [ ] User merges PR → sprint closed → team idle ready for sprint 8
## §11 — Lessons being applied from prior sprints
- **SPEC/DESIGN commit-first**: DESIGN.md rewrite is commit #2 (after sprint hygiene). No design churn mid-sprint.
- **spec-reviewer 2-pass**: APPROVED before dispatch, not after.
- **Team idle policy**: 6 agents already mounted, no shutdown until PR merged.
- **frontend-builder MUST invoke `Skill frontend-design`** before UI work (the patch commits as #1, takes effect immediately for the same sprint).