diff --git a/submissions/Victorious/.env.example b/submissions/Victorious/.env.example
new file mode 100644
index 00000000..707f2c56
--- /dev/null
+++ b/submissions/Victorious/.env.example
@@ -0,0 +1,53 @@
+# Project Victorious — environment template.
+#
+# Copy to `.env` and fill in. `.env` is gitignored and must never be committed.
+# Settings are typed in apps/api/app/core/config.py; nesting uses `__`.
+
+# --- Core --------------------------------------------------------------------
+VICTORIOUS_ENVIRONMENT=local
+
+# --- Reasoning providers ------------------------------------------------------
+# anthropic (default) | gemini | fixture
+#
+# `fixture` replays recorded responses from disk: no network, no API spend. It is
+# how the test suite runs and how the demo is insulated from provider outages.
+VICTORIOUS_LLM__PROVIDER=anthropic
+
+# Required when the provider is `anthropic`.
+ANTHROPIC_API_KEY=
+
+# Required when the provider is `gemini`.
+GOOGLE_API_KEY=
+
+# Wrap the live provider in a recorder, writing every response to the fixture
+# directory. Run once with a real provider and a real project to produce the
+# offline demo corpus, then switch VICTORIOUS_LLM__PROVIDER back to `fixture`.
+VICTORIOUS_LLM__RECORD_FIXTURES=false
+
+# Where recorded responses are read from and written to.
+VICTORIOUS_LLM__FIXTURE_DIR=./fixtures
+
+# If a live provider cannot be built (usually a missing key above), the platform
+# falls back to recorded fixtures rather than refusing to start. It reports
+# `degraded` on /health/ready and names the real backend on every agent run.
+# See docs/adr/0008-fixture-provider-and-fallback.md.
+
+# --- Persistence --------------------------------------------------------------
+# SQLite by default so a native checkout needs no services. docker-compose
+# overrides this with PostgreSQL.
+# VICTORIOUS_DATABASE__URL=postgresql+asyncpg://victorious:victorious@localhost:5432/victorious
+
+# --- Semantic memory ----------------------------------------------------------
+# Embedded ChromaDB, off until the knowledge base needs it (Milestone 5+).
+VICTORIOUS_VECTOR_STORE__ENABLED=false
+
+# --- Observability ------------------------------------------------------------
+VICTORIOUS_OBSERVABILITY__LOG_LEVEL=INFO
+# Set false locally for readable console output instead of JSON.
+VICTORIOUS_OBSERVABILITY__JSON_LOGS=true
+
+# --- Web ----------------------------------------------------------------------
+# Used by the browser. Baked in at build time for container images.
+NEXT_PUBLIC_API_URL=http://localhost:8000
+# Used by server components inside compose.
+# API_INTERNAL_URL=http://api:8000
diff --git a/submissions/Victorious/.gitignore b/submissions/Victorious/.gitignore
new file mode 100644
index 00000000..91f092ef
--- /dev/null
+++ b/submissions/Victorious/.gitignore
@@ -0,0 +1,177 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.*
+!.env.example
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+.output
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp directory
+.temp
+
+# Sveltekit cache directory
+.svelte-kit/
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# Firebase cache directory
+.firebase/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# pnpm
+.pnpm-store
+
+# yarn v3
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+# Vite files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+.vite/
+
+# Claude Code local settings (per-machine, not shared)
+.claude/settings.local.json
+
+# --- Mutagent / Helix installation --------------------------------------------
+# Developer-local tooling, installed with the Mutagent CLI. Deliberately not
+# committed: it is third-party (two skills are licensed Proprietary, and this
+# repository is MIT), it vendors ~23MB of node_modules, and nothing Victorious
+# runs depends on it — Helix is a development-time ADL conductor, never part of
+# the runtime execution path (`docs/07_System_Architecture.md`, ADR-0013).
+#
+# To restore it: npx mutagent install helix
+.claude/agents/
+.claude/skills/
+.agents/
+.codex/
+
+# Points at a specific Mutagent workspace and organization — per-developer.
+.mutagentrc.json
+
+# --- Python (apps/api) --------------------------------------------------------
+.venv/
+venv/
+__pycache__/
+*.py[cod]
+*.egg-info/
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+
+# Local databases and embedded vector store
+*.db
+*.sqlite3
+.chroma/
diff --git a/submissions/Victorious/AGENTS.md b/submissions/Victorious/AGENTS.md
new file mode 100644
index 00000000..a5f90f35
--- /dev/null
+++ b/submissions/Victorious/AGENTS.md
@@ -0,0 +1,26 @@
+
+# Helix — MutagenT ADL conductor
+
+This project has the Helix orchestrator installed. To boot it, read and adopt the agent
+definition at `.agents/skills/mutagent-helix/orchestrator.md` (run its activation-instructions: persona → system index →
+ADL dashboard), then await a `*command`.
+
+Trigger: `*mutagent` · `/mutagent-helix` · `boot`.
+
+DASHBOARD RENDERING — HARD RULE (Codex): on `*mutagent`/`boot`/`*help`/`*status`, output the
+orchestrator's `help-display-template` VERBATIM inside a fenced `text` code block. Preserve its
+EXACT shape — the boxed MUTAGENT header (box-drawing chars), every panel (lifecycle · system index ·
+setup/onboarding · state), and the command roster. Replace ONLY the `{placeholder}` tokens with
+live values; change NOTHING else. Do NOT summarize, shorten, paraphrase, drop panels, or convert it
+to Markdown headings/tables/bullets unless the operator explicitly asks for a condensed view.
+
+
+
+
+The Helix installation itself is **not committed** (`.gitignore`): it is
+third-party, partly proprietary, and vendors ~23MB of `node_modules`. Restore it
+with `npx mutagent install helix`.
+
+Helix is development-time tooling only. It is never called at runtime — see
+[ADR-0013](docs/adr/0013-engineering-review-layer.md) and
+[`docs/07_System_Architecture.md`](docs/07_System_Architecture.md).
diff --git a/submissions/Victorious/CLAUDE.md b/submissions/Victorious/CLAUDE.md
new file mode 100644
index 00000000..245ae140
--- /dev/null
+++ b/submissions/Victorious/CLAUDE.md
@@ -0,0 +1,19 @@
+
+# Helix — MutagenT ADL conductor
+
+This project has the Helix orchestrator installed. To boot it, read and adopt the agent
+definition at `.claude/skills/mutagent-helix/orchestrator.md` (run its activation-instructions: persona → system index →
+ADL dashboard), then await a `*command`.
+
+Trigger: `*mutagent` · `/mutagent-helix` · `boot`.
+
+
+
+
+The Helix installation itself is **not committed** (`.gitignore`): it is
+third-party, partly proprietary, and vendors ~23MB of `node_modules`. Restore it
+with `npx mutagent install helix`.
+
+Helix is development-time tooling only. It is never called at runtime — see
+[ADR-0013](docs/adr/0013-engineering-review-layer.md) and
+[`docs/07_System_Architecture.md`](docs/07_System_Architecture.md).
diff --git a/submissions/Victorious/DEMO.md b/submissions/Victorious/DEMO.md
new file mode 100644
index 00000000..f2a7d61c
--- /dev/null
+++ b/submissions/Victorious/DEMO.md
@@ -0,0 +1,306 @@
+# Demo Script
+
+The eleven steps of [`docs/13_Demo_and_Pitch.md`](docs/13_Demo_and_Pitch.md),
+mapped to what actually exists.
+
+**Runs with no API key, no network, and no Docker.** Everything is served from
+recorded reasoning on disk, so a provider outage cannot break the demo
+([ADR-0008](docs/adr/0008-fixture-provider-and-fallback.md)).
+
+Total runtime: **about 6 minutes**. Setup from a cold clone: **under 2 minutes**.
+
+---
+
+## Setup
+
+```bash
+# once
+cd apps/api && python -m venv .venv && .venv/Scripts/python -m pip install -e ".[dev]"
+cd ../web && npm install
+
+# every time — three terminals
+cd apps/api && .venv/Scripts/python scripts/seed_demo.py # ~15s, seeds the demo project
+cd apps/api && .venv/Scripts/python -m uvicorn app.main:app # http://localhost:8000
+cd apps/web && npm run dev # http://localhost:3000
+```
+
+The seed prints the project URL. Open `http://localhost:3000` to begin.
+
+> **If you have an API key**, set `ANTHROPIC_API_KEY` in `.env` and the same flow
+> runs against a live model. The demo does not need it, and the recorded corpus
+> is what makes the timing predictable.
+
+---
+
+## The line to land
+
+> This is not another AI coding assistant. It is an AI Software Engineering
+> Organization.
+
+Everything below argues that. The moment that proves it is **Step 8**.
+
+---
+
+## Step 1 — Landing page
+
+`http://localhost:3000`
+
+Three pillars: a specialist for every role, every artifact knows where it came
+from, you approve what matters. Below them, the coordination gap — the three
+questions `04_Existing_Solutions.md` says nothing on the market answers.
+
+> "AI made writing code fast. It did not make coordinating the decisions around
+> it fast. That layer is what this occupies."
+
+Scroll to **Engineering platform** — live backend readiness, including which
+reasoning provider is actually serving. Nothing is mocked.
+
+---
+
+## Step 2 — Create a project
+
+**Open the workspace** → the dashboard.
+
+Point at the two-field form. `07_System_Architecture.md` specifies exactly this:
+a name and a description, nothing else.
+
+> "No stack picker. No template gallery. The organization works out the
+> requirements from here."
+
+Create one live if you want to show the agents running — otherwise open the
+seeded **Hospital Management System**.
+
+---
+
+## Step 3 — Requirement discovery
+
+Project → **Advance engineering**.
+
+The Product Manager and Business Analyst run. Watch the **Engineering timeline**
+transition live — this is Server-Sent Events, not polling, and the page never
+reloads.
+
+Open **Requirements**: a PRD with `FR-01`-style identifiers and rationale, user
+stories with acceptance criteria, and a business analysis that **questions
+NFR-01** for naming no roles.
+
+> "The Business Analyst's job is to disagree where disagreement is warranted. An
+> analyst that validates everything provides no signal."
+
+---
+
+## Step 4 — The engineering organization
+
+**Organization** tab.
+
+Eight specialists, each showing status, current task, confidence, inputs,
+outputs, tokens, and duration. Expand **Show reasoning** on the Software
+Architect.
+
+> "Every agent explains itself. That is a hallucination mitigation, not a
+> feature — you can see what it was confident about and what it was not."
+
+Note who is *absent*: there is no Executive AI card. It coordinates and performs
+no engineering work, and it has no code path that could
+([ADR-0009](docs/adr/0009-orchestration-state-and-executive-boundary.md)).
+
+---
+
+## Step 5 — Architecture
+
+**Architecture** tab → **System Architecture**.
+
+A rendered Mermaid component diagram, generated by the agent. Then **Technology
+Decisions**: PostgreSQL over MongoDB, with the alternative considered and the
+trade-off accepted.
+
+> "A decision recorded without its alternatives cannot be approved on its merits.
+> It can only be rubber-stamped."
+
+---
+
+## Step 6 — Development
+
+**Development** tab → **Repository Structure**.
+
+The layout, the files written in full, and — read this one aloud — the
+**Not implemented** section.
+
+> "Authentication flows. Database migrations. The organization says what it did
+> not build. A reviewer who finds a gap you did not mention trusts nothing else
+> you wrote."
+
+Per [ADR-0006](docs/adr/0006-code-generation-depth.md) this is an inspectable
+scaffold, not a running application, and the platform never claims otherwise.
+
+---
+
+## Step 7 — Shared organizational memory
+
+**Knowledge Base** tab.
+
+All 22 artifacts, grouped by the stage that produced them. Open any one: rendered
+markdown, structured tables, version history, and provenance — which agent
+produced it, during which stage.
+
+---
+
+## Step 8 — Human approval, and the moment that matters
+
+This is the demo. Budget two minutes.
+
+1. **Requirements** → open the **PRD** → **Revise**.
+2. Before you type anything, the editor shows:
+ > ⚠ **19 artifacts** depend on this and would go out of date · 7 stages would rerun
+3. Change a requirement. Save.
+4. **Traceability** tab — the graph, laid out by lifecycle stage. **19 of 22
+ nodes and their edges are now highlighted as out of date.** Click one to focus
+ its dependencies.
+5. **Advance engineering.** The organization does not just carry on:
+ > *Awaiting human approval: Approve re-synchronisation of work that is now out of date*
+6. **Approvals** → approve it. Advance again. Seven stages rebuild.
+7. Back to **Traceability**: **zero stale artifacts.**
+
+> "Change one requirement, and the organization tells you exactly what no longer
+> reflects it, asks permission before rebuilding, and then converges. That is the
+> question no coding assistant answers."
+
+If asked how it knows: staleness is never stored. An artifact is out of date when
+a traceability edge cites an older version than its upstream currently has, so it
+is computed from the graph on every read and cannot drift
+([ADR-0007](docs/adr/0007-traceability-model.md)).
+
+---
+
+## Step 9 — Testing
+
+**Testing** tab → **Coverage Report**.
+
+> "1 of 2 requirements covered — and it says *why*: FR-01 has no acceptance
+> criteria, so it cannot be tested as written."
+
+Coverage is measured against **requirements**, not lines. An uncovered
+requirement is visible as a gap rather than hidden behind a percentage.
+
+---
+
+## Step 10 — Documentation
+
+**Documentation** tab.
+
+README, API reference, architecture narrative, developer guide, changelog — all
+generated from what the organization actually decided, and all consistent with
+the scaffold being a scaffold.
+
+---
+
+## Step 11 — Deployment preparation
+
+**Documentation** tab → **Deployment Plan**.
+
+Checklist, environment variables **by name and purpose only**, containerisation,
+rollback, and an **Outstanding before production** section that names what
+genuinely blocks a release.
+
+> "It lists no credential values. A deployment document is exactly where a secret
+> gets committed by accident."
+
+---
+
+## Step 12 — Helix Review
+
+**Helix Review** tab.
+
+**Overall 90/100** across all 22 artifacts, scored per specialist — Business
+Analyst 86, Software Architect 92 — with recommendations that name something
+real: *"add a uniqueness constraint on (doctor, slot) to enforce FR-02"*.
+
+Scroll one review open. Each finding is tagged `check` or `reasoning`.
+
+> "Most of this number is measured, not opined. Five deterministic checks over
+> the artifact and its trace edges carry the full hundred points — does it
+> declare upstream, does it carry structured fields, is it substantive, what
+> confidence did the agent report, does it have the fields its type requires. A
+> reasoning pass then reads the artifact *and those findings*, and can move the
+> score by at most twelve points, in writing."
+
+> "That cap is the point. A model can sharpen a judgement; it cannot overturn a
+> fact, and it cannot manufacture a good score for an artifact that declares no
+> upstream. It is also why these scores actually differ — 81 to 100 across
+> eleven distinct values. A purely generative reviewer replaying fixtures would
+> score everything the same, and the number would be theatre."
+
+Click any artifact → its review sits beside the version history, judging *that
+version*. Revise it by hand and the review goes empty rather than inheriting the
+old score.
+
+> "And the Executive consults these before letting a specialist build on upstream
+> work — scoped to what that stage actually reads, so a weak deployment plan
+> can't block architecture. Advisory by default; one environment variable makes
+> it a gate."
+
+**If asked where Helix is:** the reviewer runs natively, on the same provider
+abstraction the agents use. Helix has no importable code — no `.py` files, no
+server, its orchestrator is a markdown definition for a coding agent — and
+`07_System_Architecture.md` keeps Mutagent out of the runtime path regardless.
+Helix specs and evaluates this reviewer at development time.
+[ADR-0013](docs/adr/0013-engineering-review-layer.md).
+
+---
+
+## Closing
+
+> "Not a chatbot. Not a coding assistant. Not a project management tool. An AI
+> Software Engineering Organization that coordinates the lifecycle — and can tell
+> you what a change breaks."
+
+---
+
+## If a judge asks
+
+**"Is this real, or scripted?"**
+Recorded reasoning, replayed — so the demo is deterministic and needs no network.
+Set `ANTHROPIC_API_KEY` and the identical flow runs against a live model; the
+provider is one environment variable
+([ADR-0004](docs/adr/0004-llm-provider-default.md)). `/health/ready` names the
+backend actually serving.
+
+**"Can I run the generated code?"**
+No, and the platform never claims you can. It is an inspectable scaffold traced
+to the decisions that produced it. That was a deliberate scope choice —
+[ADR-0006](docs/adr/0006-code-generation-depth.md) explains it, and the budget
+went to change propagation instead, which is what no other tool does.
+
+**"How do I know the architecture is what you say it is?"**
+```bash
+cd apps/api && .venv/Scripts/python -m pytest tests/test_architecture.py -v
+```
+Eleven rules, parsed from the source tree. A layer importing outward, a framework
+reaching the domain, or the DI container built outside the composition root fails
+the build.
+
+**"What did you get wrong?"**
+```bash
+apps/api/.venv/Scripts/python evaluation/adl_cycle.py
+```
+Re-synchronisation was divergent — rebuilding stale work left *more* stale
+derivations than before it, 19 → 167. Found by evaluation, diagnosed to
+superseded trace edges, fixed, re-measured at 0. Written up in
+[`evaluation/optimization-report.md`](evaluation/optimization-report.md) with
+five other cycles.
+
+**"What is missing?"**
+Authentication, verified container deployment, and prompts tuned against a live
+model. All three are in the README's [Known gaps](README.md#known-gaps).
+
+---
+
+## Fallbacks
+
+| If | Then |
+|---|---|
+| The seed is missing | `cd apps/api && .venv/Scripts/python scripts/seed_demo.py` |
+| A page shows *API unreachable* | The API terminal died. Restart uvicorn; the page recovers on reload |
+| The live stream shows *Reconnecting* | Harmless — `EventSource` retries and replays what was missed |
+| You want to demo from a decision point | Reseed with `--at-gate` |
+| Anything looks stale unexpectedly | It probably is. Open **Traceability** and show why — that is the product |
diff --git a/submissions/Victorious/LICENSE b/submissions/Victorious/LICENSE
new file mode 100644
index 00000000..52207957
--- /dev/null
+++ b/submissions/Victorious/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 HackIndia
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/submissions/Victorious/README.md b/submissions/Victorious/README.md
new file mode 100644
index 00000000..3d4d4f76
--- /dev/null
+++ b/submissions/Victorious/README.md
@@ -0,0 +1,316 @@
+# Project Victorious
+
+An AI-native **Software Engineering Workspace**, powered by an autonomous AI
+Software Engineering Organization.
+
+Specialized engineering agents — a Product Manager, Business Analyst, Software
+Architect, Full Stack Engineer, QA Engineer, and Documentation Agent, coordinated
+by an Executive AI — transform a software idea into a structured engineering
+project. They work over a shared organizational memory, keep every artifact
+traceable to the decision that produced it, and stop at human approval gates
+before critical decisions.
+
+This is not an AI coding assistant. Coding assistants make implementation fast;
+they do not answer *"which downstream artifacts does this requirement change
+invalidate?"* That question is what this platform exists to answer.
+
+Built for the **Mutagent Challenge** (HackIndia Spark 11, Hyderabad).
+
+---
+
+## Status
+
+**All ten milestones complete.**
+
+The whole platform runs end to end with **no API key and no network**: create a
+project from a name and a description, watch the organization work through nine
+lifecycle stages *as it happens*, approve three gates, and read all 22 generated
+artifacts in the browser — rendered markdown, tables, Mermaid component diagrams,
+code, and full version history.
+
+Agent cards and the Engineering Timeline update live over Server-Sent Events, with
+no polling and no page refresh ([ADR-0011](docs/adr/0011-stream-carries-signals-not-state.md)).
+
+**The capability the whole platform exists for now works, verified against a live
+server.** On a finished 8-stage project, revising one requirement marks **19
+artifacts out of date**; the organization proposes re-synchronisation; approving
+it reruns seven stages against the new requirement and clears every stale
+artifact. That is the question
+[`04_Existing_Solutions.md`](docs/04_Existing_Solutions.md) says nothing on the
+market answers. See
+[ADR-0012](docs/adr/0012-change-propagation-and-resynchronisation.md).
+
+The **Traceability** view draws that graph — 22 artifacts laid out by lifecycle
+stage, with stale derivations highlighted and a click to focus one artifact's
+dependencies. Before revising anything, the workspace shows the blast radius:
+*19 artifacts depend on this and would go out of date · 7 stages would rerun*.
+
+```bash
+cd apps/api && .venv/Scripts/python scripts/seed_demo.py # seeds the demo project
+cd apps/api && .venv/Scripts/python -m uvicorn app.main:app # http://localhost:8000
+cd apps/web && npm run dev # http://localhost:3000
+```
+
+Cold start from an empty database to a fully populated workspace: **18 seconds**.
+The presentation walkthrough is [`DEMO.md`](DEMO.md).
+
+**Every artifact is reviewed as it lands.** The **Helix Review** tab scores all
+22 — overall **90/100**, per specialist from 86 to 92, with specific
+recommendations ("add a uniqueness constraint on (doctor, slot) to enforce
+FR-02"). Most of each score is *measured*: five deterministic checks over the
+artifact and its trace edges carry the full 100 points, and a reasoning pass may
+move the result by at most **±12**, in writing. So a model can sharpen a
+judgement but cannot overturn a fact — and scores genuinely differ, spanning
+**81–100 across eleven distinct values** rather than the flat number a purely
+generative reviewer would produce on recorded fixtures. Every finding is labelled
+`check` or `reasoning`, because only one of the two is reproducible.
+
+The reviewer runs **natively**, on the same provider abstraction the specialists
+use. Helix — Mutagent's ADL conductor — specs and evaluates it at development
+time and stays out of the request path, as
+[`07_System_Architecture.md`](docs/07_System_Architecture.md) requires. See
+[ADR-0013](docs/adr/0013-engineering-review-layer.md).
+
+**Not yet implemented: authentication.** `09_MVP_Roadmap.md` lists it as
+Priority 1 and it is not built — the API is currently unauthenticated. See
+[Known gaps](#known-gaps).
+
+---
+
+**Milestone 4 — the engineering organization is operational.**
+
+A project now runs end to end: an idea becomes requirements, validated
+requirements become an architecture, an approved architecture becomes a plan, a
+scaffold, a test suite, documentation, and a deployment plan — through three
+human approval gates, with every artifact traced to what it was derived from.
+
+On the `13_Demo_and_Pitch.md` hospital scenario that is **22 artifacts across 8
+agent runs and 3 approval gates**, with the full traceability graph connecting
+them.
+
+Foundations behind it: architectural boundaries and DI (M0); shared organizational
+memory with append-only versioning and the traceability graph (M1); the
+provider abstraction and agent execution framework (M2); the Executive AI and its
+LangGraph workflow (M3).
+
+The workspace UI arrives in Milestone 5 — until then everything is exercised
+through the API and the test suite.
+
+Four properties are load-bearing:
+
+- **Staleness is computed, not stored.** The traceability model answers the
+ question [`04_Existing_Solutions.md`](docs/04_Existing_Solutions.md) says
+ nothing on the market answers — *which downstream artifacts does this
+ requirement change invalidate?* — because an artifact is stale when a
+ traceability edge cites an older version than its upstream currently has.
+ See [ADR-0007](docs/adr/0007-traceability-model.md).
+- **No agent can produce an orphan.** The agent base class rejects any artifact
+ that fails to declare the upstream it was derived from, so nothing can be
+ invisible to impact analysis.
+- **The Executive AI cannot perform engineering work.** It lives in the
+ orchestration layer with no artifact-writing path at all, so
+ [`15_Development_Guidelines.md`](docs/15_Development_Guidelines.md)'s boundary
+ is structural rather than a matter of discipline. A test asserts that no
+ artifact and no agent run is ever owned by the Executive role. See
+ [ADR-0009](docs/adr/0009-orchestration-state-and-executive-boundary.md).
+- **Documents cannot drift from their data.** Agents emit structured fields; the
+ readable artifact is *rendered* from those fields rather than written
+ separately, so what a human reads and what the next agent consumes are the
+ same information. See
+ [ADR-0010](docs/adr/0010-agent-roster-and-stage-ownership.md).
+
+See [`docs/09_MVP_Roadmap.md`](docs/09_MVP_Roadmap.md) for scope and
+[`docs/adr/`](docs/adr/README.md) for decisions and deviations taken so far.
+
+---
+
+## Running it
+
+Requires Python 3.12+ and Node 22+. No container runtime needed.
+
+**API** — from `apps/api`:
+
+```bash
+python -m venv .venv
+.venv/Scripts/python -m pip install -e ".[dev]" # Windows
+# source .venv/bin/activate && pip install -e ".[dev]" # macOS / Linux
+.venv/Scripts/python -m uvicorn app.main:app --reload
+```
+
+Serves on `http://localhost:8000`. Interactive API docs at `/docs`.
+
+**Web** — from `apps/web`:
+
+```bash
+npm install
+npm run dev
+```
+
+Serves on `http://localhost:3000`.
+
+**Configuration** — copy `.env.example` to `.env`. **No API key is required to
+run anything.** Without one the platform falls back to the `fixture` provider,
+which replays recorded reasoning from disk; `/health/ready` reports `degraded` and
+names the real backend, so the fallback is never silent. See
+[ADR-0008](docs/adr/0008-fixture-provider-and-fallback.md).
+
+**Containers** — `docker compose up --build` runs the full stack with PostgreSQL.
+This path is written but **not yet verified** (no Docker on the development
+machine); see [ADR-0005](docs/adr/0005-runtime-infrastructure-deviations.md).
+
+---
+
+## Quality gates
+
+From `apps/api`:
+
+```bash
+.venv/Scripts/python -m pytest # tests, including architecture rules
+.venv/Scripts/python -m ruff check . # lint
+.venv/Scripts/python -m mypy app # strict type checking
+```
+
+From `apps/web`:
+
+```bash
+npm run typecheck
+npm run lint
+npm run build
+```
+
+`tests/test_architecture.py` is worth a look: it parses the source tree and fails
+the build if a layer imports outward, if the domain layer picks up a framework
+dependency, or if the DI container is constructed outside the composition root.
+The architecture is enforced, not just documented.
+
+---
+
+## Repository layout
+
+```
+apps/
+ api/ FastAPI — agents, orchestration, shared memory
+ app/
+ domain/ Pure domain layer: no frameworks, no I/O
+ core/ Config, logging, DI container, errors, health
+ db/ SQLAlchemy models, session, Alembic migrations
+ memory/ Shared organizational memory + agent context assembly
+ events/ Event bus (durable append + live fan-out)
+ llm/ Provider abstraction: Anthropic, Gemini, fixture replay
+ agents/ The eight engineering agents, their contracts and prompts
+ review/ Engineering review: deterministic checks + bounded reasoning
+ orchestration/ Executive AI, workflow graph, dependency & conflict rules
+ api/ HTTP transport only
+ tests/
+ web/ Next.js — the engineering workspace
+docs/ Specification (read-only) — see docs/adr/ for decisions
+evaluation/ Mutagent ADL artifacts (Milestone 9)
+```
+
+Dependencies point inward: `api → orchestration → agents → memory → domain`.
+`review` is a sibling of `agents`, not a layer above it — it may not import the
+API or orchestration, and `tests/test_architecture.py` enforces that.
+
+**Database migrations** — from `apps/api`:
+
+```bash
+.venv/Scripts/python -m alembic upgrade head # apply
+.venv/Scripts/python -m alembic downgrade base # reverse
+```
+
+Outside production the app creates any missing tables on startup, so no migration
+step is needed for local development.
+
+---
+
+## Documentation
+
+The [`docs/`](docs/) directory is the authoritative specification, treated as
+read-only input by this implementation. Start with
+[`14_Executive_Summary.md`](docs/14_Executive_Summary.md), then
+[`05_AI_Agent_Architecture.md`](docs/05_AI_Agent_Architecture.md) and
+[`09_MVP_Roadmap.md`](docs/09_MVP_Roadmap.md).
+
+[`docs/adr/`](docs/adr/README.md) records every decision the specification did
+not settle, every deviation from it, and the specification gaps found during
+review.
+
+---
+
+## Evaluation
+
+The platform is scored by a runnable harness in [`evaluation/`](evaluation/README.md),
+the evidence `02_Proposed_Solution.md` requires of a project built through
+Mutagent's Agentic Development Lifecycle. Everything runs offline.
+
+```bash
+apps/api/.venv/Scripts/python evaluation/run_evaluation.py # scorecards
+apps/api/.venv/Scripts/python evaluation/adl_cycle.py # one ADL cycle, measured
+```
+
+Eight deterministic scorers over three project briefs, **93.8% overall**. No
+scorer asks a language model to judge quality — a hallucinating grader would
+report improvement that did not happen.
+
+The documented cycle found that re-synchronisation was **divergent**: rebuilding
+stale work left *more* stale derivations than before it, because superseded trace
+edges still cited the versions they originally consumed. **167 → 0** after the
+fix. See [`evaluation/optimization-report.md`](evaluation/optimization-report.md).
+
+---
+
+## Known gaps
+
+Stated plainly rather than discovered later.
+
+- **No authentication.** `09_MVP_Roadmap.md` lists it Priority 1; it is not
+ implemented, so the API accepts any caller. Acceptable for local development
+ and a demo, not for deployment. Scheduled before release.
+- **Docker compose is unverified.** The Dockerfiles and compose file are written
+ but have never been run — Docker is not installed on the development machine.
+ See [ADR-0005](docs/adr/0005-runtime-infrastructure-deviations.md).
+- **Prompts are untuned against a live model.** Every test and the demo corpus
+ run on recorded fixtures. The contracts and plumbing are verified; the quality
+ of real model output is not yet.
+- **The traceability graph is over-connected.** Agents declare their sources once
+ per run, so a late agent that read sixteen artifacts cites all sixteen. The
+ edges are accurate but impact analysis is less discriminating downstream. See
+ [ADR-0010](docs/adr/0010-agent-roster-and-stage-ownership.md).
+- **No syntax highlighting** in rendered code blocks — legible monospace only.
+- **The Mutagent evaluator package could not be installed.** `mutagent install
+ evaluator` fails on Windows with `spawn npm ENOENT` even with npm on PATH — the
+ CLI spawns `npm` rather than `npm.cmd`. Reported through `mutagent feedback
+ send`. The ADL methodology was followed regardless, with the evidence produced
+ in-repo.
+
+---
+
+## Relationship with Mutagent
+
+Mutagent is the engineering framework used to *develop* Project Victorious,
+through its Agentic Development Lifecycle: Specification, Build, Evaluation,
+Diagnosis, Optimization.
+
+Project Victorious is the system being developed. It is not another Mutagent, and
+it does not reimplement Helix. Mutagent develops AI systems; Victorious develops
+software products. Mutagent is not part of the runtime.
+
+That boundary was tested directly when the **Helix** package was installed into
+this repository. The tempting integration — have Helix review each artifact at
+runtime — is not possible and not permitted: Helix ships no importable code (zero
+`.py` files, every package `"private": true`, no server or daemon; its
+orchestrator is a markdown agent definition adopted by a coding agent), and
+`07_System_Architecture.md` places Mutagent outside the execution path either
+way.
+
+So the **engineering review layer** (`apps/api/app/review/`) is first-party, built
+on the same `LLMProvider` abstraction as the specialists, and
+`tests/test_architecture.py` enforces its boundaries with the rest. Helix keeps
+the role the documentation gives it: at development time it specs, evaluates, and
+optimizes that reviewer. Full reasoning in
+[ADR-0013](docs/adr/0013-engineering-review-layer.md).
+
+The Helix installation itself (`.claude/`, `.agents/`, `.codex/`) is
+developer-local tooling installed through the Mutagent CLI and is not committed
+here — it is third-party, partly proprietary, and not a dependency of anything
+Victorious runs.
diff --git a/submissions/Victorious/apps/api/.dockerignore b/submissions/Victorious/apps/api/.dockerignore
new file mode 100644
index 00000000..f9e8ca75
--- /dev/null
+++ b/submissions/Victorious/apps/api/.dockerignore
@@ -0,0 +1,11 @@
+.venv/
+__pycache__/
+*.pyc
+*.pyo
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+tests/
+*.db
+.chroma/
+.env
diff --git a/submissions/Victorious/apps/api/Dockerfile b/submissions/Victorious/apps/api/Dockerfile
new file mode 100644
index 00000000..37d03971
--- /dev/null
+++ b/submissions/Victorious/apps/api/Dockerfile
@@ -0,0 +1,51 @@
+# syntax=docker/dockerfile:1
+
+# --- Builder -----------------------------------------------------------------
+# Dependencies are installed into a virtualenv here and copied into a clean
+# runtime stage, so build tooling never reaches the final image.
+FROM python:3.12-slim AS builder
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1
+
+WORKDIR /build
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+# Copied before the source so a code change does not invalidate the dependency
+# layer on every rebuild.
+COPY pyproject.toml ./
+RUN --mount=type=cache,target=/root/.cache/pip \
+ pip install --upgrade pip && pip install .
+
+COPY app ./app
+RUN pip install --no-deps .
+
+# --- Runtime -----------------------------------------------------------------
+FROM python:3.12-slim AS runtime
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PATH="/opt/venv/bin:$PATH"
+
+# Runs unprivileged: a container compromise should not imply root.
+RUN groupadd --system victorious \
+ && useradd --system --gid victorious --create-home victorious
+
+WORKDIR /app
+
+COPY --from=builder /opt/venv /opt/venv
+COPY --chown=victorious:victorious app ./app
+
+USER victorious
+
+EXPOSE 8000
+
+# Uses the liveness probe deliberately — readiness depends on the database, and a
+# database outage should drain traffic, not trigger a container restart loop.
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+ CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=4).status == 200 else 1)"
+
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/submissions/Victorious/apps/api/alembic.ini b/submissions/Victorious/apps/api/alembic.ini
new file mode 100644
index 00000000..a7216c5e
--- /dev/null
+++ b/submissions/Victorious/apps/api/alembic.ini
@@ -0,0 +1,47 @@
+# Alembic configuration.
+#
+# The database URL is deliberately absent: env.py reads it from the application
+# settings so migrations run against exactly the database the app uses, with no
+# second place for a connection string to drift out of sync.
+
+[alembic]
+script_location = app/db/migrations
+prepend_sys_path = .
+version_path_separator = os
+
+# Timestamped, slugged filenames so the migration history reads chronologically.
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(slug)s
+
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARNING
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARNING
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/submissions/Victorious/apps/api/app/__init__.py b/submissions/Victorious/apps/api/app/__init__.py
new file mode 100644
index 00000000..406f3acb
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/__init__.py
@@ -0,0 +1,16 @@
+"""Project Victorious API.
+
+An AI-native Software Engineering Organization: specialized engineering agents
+coordinated by an Executive AI over a shared organizational memory, with full
+artifact traceability and human approval gates.
+
+Layering (dependencies point inward only):
+
+ api -> orchestration -> agents -> memory -> domain
+ core -> (cross-cutting: config, logging, DI, errors, health)
+
+``domain`` is pure: it imports nothing from the layers above it and no third-party
+framework. ``tests/test_architecture.py`` enforces this mechanically.
+"""
+
+__version__ = "0.1.0"
diff --git a/submissions/Victorious/apps/api/app/agents/__init__.py b/submissions/Victorious/apps/api/app/agents/__init__.py
new file mode 100644
index 00000000..61dbfa42
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/__init__.py
@@ -0,0 +1,65 @@
+"""The AI engineering organization.
+
+One module per engineering specialist, each a subclass of :class:`BaseAgent` with
+its own typed output contract and its own versioned prompt. Eight agents fill the
+seven MVP roles from `09_MVP_Roadmap.md`; see :mod:`app.agents.organization`.
+"""
+
+from app.agents.base import BaseAgent
+from app.agents.business_analyst import BusinessAnalystAgent, BusinessAnalystOutput
+from app.agents.contracts import (
+ AgentOutput,
+ AgentResult,
+ ArtifactDraft,
+ TraceLink,
+)
+from app.agents.documentation import (
+ DeploymentPreparationAgent,
+ DeploymentPreparationOutput,
+ DocumentationAgent,
+ DocumentationOutput,
+)
+from app.agents.full_stack_engineer import (
+ FullStackEngineerAgent,
+ FullStackEngineerOutput,
+)
+from app.agents.organization import AGENT_CLASSES, build_organization
+from app.agents.product_manager import ProductManagerAgent, ProductManagerOutput
+from app.agents.prompts import PromptError, available_prompts, load_prompt, render_prompt
+from app.agents.qa_engineer import QAEngineerAgent, QAEngineerOutput
+from app.agents.software_architect import (
+ ArchitectOutput,
+ ImplementationPlannerAgent,
+ ImplementationPlanOutput,
+ SoftwareArchitectAgent,
+)
+
+__all__ = [
+ "AGENT_CLASSES",
+ "AgentOutput",
+ "AgentResult",
+ "ArchitectOutput",
+ "ArtifactDraft",
+ "BaseAgent",
+ "BusinessAnalystAgent",
+ "BusinessAnalystOutput",
+ "DeploymentPreparationAgent",
+ "DeploymentPreparationOutput",
+ "DocumentationAgent",
+ "DocumentationOutput",
+ "FullStackEngineerAgent",
+ "FullStackEngineerOutput",
+ "ImplementationPlanOutput",
+ "ImplementationPlannerAgent",
+ "ProductManagerAgent",
+ "ProductManagerOutput",
+ "PromptError",
+ "QAEngineerAgent",
+ "QAEngineerOutput",
+ "SoftwareArchitectAgent",
+ "TraceLink",
+ "available_prompts",
+ "build_organization",
+ "load_prompt",
+ "render_prompt",
+]
diff --git a/submissions/Victorious/apps/api/app/agents/base.py b/submissions/Victorious/apps/api/app/agents/base.py
new file mode 100644
index 00000000..1d2773b6
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/base.py
@@ -0,0 +1,481 @@
+"""Base class for every engineering agent.
+
+Defines one execution template that all seven MVP agents follow, so an agent
+implementation supplies only what is genuinely specific to its role: its output
+contract, its prompt, and how it names the artifacts it produces.
+
+Everything that must be true of *every* agent lives here and cannot be skipped:
+
+- an :class:`AgentRun` is recorded before reasoning starts, so an agent that
+ fails or hangs is still visible in the Organization view rather than absent;
+- reasoning is validated against a typed contract;
+- **artifacts cannot be written without declaring their upstream** — the orphan
+ guard ADR-0007 requires;
+- trace edges are written from those declarations;
+- events are published at each transition, feeding the timeline and live stream;
+- failures mark the run failed and publish, rather than vanishing.
+
+`05_AI_Agent_Architecture.md` requires each agent to be "an independent, reusable
+module with clearly defined inputs, outputs, responsibilities, and communication
+interfaces", and that agents not modify each other's state. Agents here reach
+shared memory only through this base class.
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from datetime import UTC, datetime
+
+from app.agents.contracts import AgentOutput, AgentResult, ArtifactDraft, TraceLink
+from app.agents.prompts import load_prompt
+from app.core.logging import get_correlation_id, get_logger
+from app.domain.agents import AgentRun, AgentRunStatus
+from app.domain.artifacts import Artifact, ArtifactStatus, ArtifactVersion
+from app.domain.errors import ValidationError
+from app.domain.events import EventType, ProjectEvent
+from app.domain.lifecycle import ROLE_TITLES, AgentRole, LifecycleStage
+from app.domain.traceability import TraceEdge
+from app.events.bus import EventBus
+from app.llm.provider import (
+ CompletionRequest,
+ LLMProvider,
+ Message,
+ Role,
+ StructuredResponse,
+)
+from app.memory.context_builder import ContextBuilder, ProjectContext
+from app.memory.repository import SharedMemory
+from app.review.reviewer import EngineeringReviewer
+
+logger = get_logger(__name__)
+
+SYSTEM_PROMPT = "engineering_organization"
+
+
+class BaseAgent[TOutput: AgentOutput](ABC):
+ """One engineering specialist.
+
+ Subclasses declare their role, stage, output contract, and prompt, then
+ implement :meth:`build_task` to describe the work. Execution, persistence,
+ traceability, and observability are handled here.
+ """
+
+ #: The engineering role this agent fills.
+ role: AgentRole
+
+ #: The lifecycle stage it performs.
+ stage: LifecycleStage
+
+ #: Pydantic contract its reasoning is validated against.
+ output_model: type[TOutput]
+
+ #: Filename stem under ``app/agents/prompts/``.
+ prompt_name: str
+
+ def __init__(
+ self,
+ memory: SharedMemory,
+ provider: LLMProvider,
+ context_builder: ContextBuilder,
+ events: EventBus,
+ reviewer: EngineeringReviewer | None = None,
+ ) -> None:
+ self._memory = memory
+ self._provider = provider
+ self._context = context_builder
+ self._events = events
+ # Optional so an agent can be constructed without one. Reviewing is a
+ # quality signal layered on top of production, never a precondition for
+ # it, and an agent with no reviewer behaves exactly as it did before the
+ # layer existed.
+ self._reviewer = reviewer
+
+ @property
+ def title(self) -> str:
+ """Human-facing role name, used in events and the Organization view."""
+ return ROLE_TITLES[self.role]
+
+ @abstractmethod
+ def build_task(self, context: ProjectContext) -> str:
+ """Return the instruction describing this invocation's work.
+
+ Receives the assembled context so the task can reference what is actually
+ present — asking an agent to "revise the architecture" when none exists
+ wastes an invocation.
+ """
+
+ def describe_task(self) -> str:
+ """Return a short label for the Organization view. Overridable."""
+ return f"{self.title} · {self.stage.value.replace('_', ' ')}"
+
+ def compose_artifacts(
+ self, output: TOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ """Turn validated reasoning into the artifacts to persist.
+
+ The default writes whatever the model emitted in ``output.artifacts``.
+
+ Concrete agents override this to render their artifacts from the
+ *structured* fields of their own contract instead. Two reasons: a
+ rendered document then cannot drift from the data downstream agents
+ read, and the model spends its output budget on engineering content
+ rather than on re-formatting the same information as prose.
+ """
+ return list(output.artifacts)
+
+ @staticmethod
+ def _links(output: AgentOutput) -> list[TraceLink]:
+ """Upstream declarations to attach to every artifact from this run."""
+ return list(output.sources)
+
+ async def run(self, project_id: str, *, feedback: str | None = None) -> AgentResult:
+ """Execute one unit of engineering work.
+
+ Args:
+ project_id: Project to work on.
+ feedback: Reviewer feedback from a rejected approval. Supplied on
+ re-run so a rejection teaches rather than repeats.
+
+ Returns:
+ The persisted result.
+
+ Raises:
+ VictoriousError: on reasoning or persistence failure. The run is
+ marked failed and an event published before the error propagates.
+ """
+ context = await self._context.build(project_id, stage=self.stage, role=self.role)
+
+ run = await self._memory.runs.create(
+ AgentRun(
+ project_id=project_id,
+ role=self.role,
+ stage=self.stage,
+ status=AgentRunStatus.ACTIVE,
+ task=self.describe_task(),
+ input_artifact_ids=context.artifact_ids,
+ provider=self._provider.name,
+ model=self._provider.model,
+ correlation_id=get_correlation_id(),
+ )
+ )
+
+ await self._publish(
+ project_id,
+ EventType.AGENT_STARTED,
+ f"{self.title} started: {self.describe_task()}",
+ {"run_id": run.id, "input_artifacts": len(context.artifact_ids)},
+ )
+
+ try:
+ response = await self._reason(context, feedback)
+ output = response.value
+ drafts = self.compose_artifacts(output, context)
+ artifact_ids, edge_ids = await self._persist(
+ project_id, run, context, output, drafts
+ )
+ except Exception as exc:
+ await self._fail(run, exc)
+ raise
+
+ run.status = AgentRunStatus.COMPLETED
+ run.confidence = output.confidence
+ run.reasoning_summary = output.reasoning
+ run.output_artifact_ids = artifact_ids
+ # Recorded per run because `12_Risk_Analysis.md` rates High Token
+ # Consumption a Medium risk. Measuring it is the precondition for the
+ # caching decision deferred in ADR-0005 — the intent is to decide from
+ # data rather than assumption.
+ run.token_usage = response.usage
+ run.provider = response.provider
+ run.model = response.model
+ # Recorded on the run so the Executive AI can raise the gate on its next
+ # assessment. An agent's request for review must survive the return trip.
+ run.requires_approval = output.requires_approval
+ run.approval_reason = output.approval_reason
+ run.completed_at = datetime.now(UTC)
+ await self._memory.runs.update(run)
+
+ await self._publish(
+ project_id,
+ EventType.AGENT_COMPLETED,
+ f"{self.title} completed with {len(artifact_ids)} artifact(s)",
+ {
+ "run_id": run.id,
+ "confidence": output.confidence,
+ "artifact_ids": artifact_ids,
+ "concerns": len(output.concerns),
+ },
+ )
+
+ if output.concerns:
+ await self._publish(
+ project_id,
+ EventType.CONFLICT_DETECTED,
+ f"{self.title} raised {len(output.concerns)} concern(s) about upstream work",
+ {"run_id": run.id, "concerns": output.concerns},
+ )
+
+ logger.info(
+ "Agent completed",
+ extra={
+ "role": self.role.value,
+ "stage": self.stage.value,
+ "run_id": run.id,
+ "confidence": output.confidence,
+ "artifacts": len(artifact_ids),
+ },
+ )
+
+ return AgentResult(
+ run_id=run.id,
+ output=output,
+ artifact_ids=artifact_ids,
+ edge_ids=edge_ids,
+ )
+
+ async def _reason(
+ self, context: ProjectContext, feedback: str | None
+ ) -> StructuredResponse[TOutput]:
+ """Invoke the provider and return the validated response.
+
+ Returns the whole response rather than just its value, so token usage and
+ the backend that actually served the request are recorded on the run.
+ """
+ messages = [Message(role=Role.USER, content=self._compose_user_message(context))]
+
+ if feedback:
+ messages.append(
+ Message(
+ role=Role.USER,
+ content=(
+ "A reviewer rejected your previous output with this feedback:\n\n"
+ f"{feedback}\n\n"
+ "Produce a revised version that addresses it directly."
+ ),
+ )
+ )
+
+ return await self._provider.complete_structured(
+ CompletionRequest(
+ system=f"{load_prompt(SYSTEM_PROMPT)}\n\n{load_prompt(self.prompt_name)}",
+ messages=messages,
+ # Keyed by role and stage so recorded fixtures are named after
+ # the work they represent and can be read and edited by hand.
+ fixture_key=f"{self.role.value}.{self.stage.value}",
+ metadata={"role": self.role.value, "stage": self.stage.value},
+ ),
+ self.output_model,
+ )
+
+ def _compose_user_message(self, context: ProjectContext) -> str:
+ """Combine assembled context with this invocation's task."""
+ return f"{context.render()}\n\n---\n\n# Your task\n\n{self.build_task(context)}"
+
+ async def _persist(
+ self,
+ project_id: str,
+ run: AgentRun,
+ context: ProjectContext,
+ output: AgentOutput,
+ drafts: list[ArtifactDraft],
+ ) -> tuple[list[str], list[str]]:
+ """Write artifacts and their trace edges.
+
+ Returns:
+ Artifact IDs and edge IDs created.
+ """
+ available = set(context.artifact_ids)
+ artifact_ids: list[str] = []
+ edge_ids: list[str] = []
+
+ for draft in drafts:
+ self._guard_against_orphan(draft, available)
+
+ # An agent that runs again — after a rejection, or because a
+ # requirement changed — revises what it produced before rather than
+ # creating a competing copy. The artifact keeps its identity, so
+ # every traceability edge pointing at it survives the revision, and
+ # the previous version stays readable (ADR-0007).
+ artifact = await self._memory.artifacts.find_by_identity(
+ project_id, draft.type, self.stage, draft.title
+ )
+ revising = artifact is not None
+
+ if artifact is None:
+ artifact = await self._memory.artifacts.create(
+ Artifact(
+ project_id=project_id,
+ type=draft.type,
+ title=draft.title,
+ stage=self.stage,
+ owner_role=self.role,
+ status=ArtifactStatus.DRAFT,
+ )
+ )
+ elif artifact.status is not ArtifactStatus.DRAFT:
+ # Revised work has not been reviewed yet, whatever its previous
+ # status was. Leaving it approved would let a rejection be
+ # answered with content nobody signed off on.
+ artifact.status = ArtifactStatus.DRAFT
+ await self._memory.artifacts.update(artifact)
+
+ await self._memory.artifacts.append_version(
+ artifact.id,
+ ArtifactVersion(
+ artifact_id=artifact.id,
+ version=1, # Assigned by the repository; ignored here.
+ body_markdown=draft.body_markdown,
+ content=dict(draft.content),
+ produced_by_run_id=run.id,
+ summary=draft.summary,
+ confidence=output.confidence,
+ ),
+ )
+ artifact_ids.append(artifact.id)
+
+ for link in draft.derived_from:
+ upstream = await self._memory.artifacts.get(link.upstream_artifact_id)
+ edge = await self._memory.traces.add_edge(
+ TraceEdge(
+ project_id=project_id,
+ upstream_artifact_id=upstream.id,
+ downstream_artifact_id=artifact.id,
+ kind=link.kind,
+ # The version actually consumed. ADR-0007: this is what
+ # makes staleness computable when the upstream advances.
+ upstream_version=upstream.current_version,
+ created_by_run_id=run.id,
+ rationale=link.rationale,
+ )
+ )
+ edge_ids.append(edge.id)
+
+ await self._publish(
+ project_id,
+ EventType.ARTIFACT_REVISED if revising else EventType.ARTIFACT_CREATED,
+ f"{self.title} {'revised' if revising else 'produced'} {draft.title}",
+ {
+ "artifact_id": artifact.id,
+ "artifact_type": draft.type.value,
+ "upstream_count": len(draft.derived_from),
+ },
+ )
+
+ await self._review(artifact, len(draft.derived_from))
+
+ return artifact_ids, edge_ids
+
+ async def _review(self, artifact: Artifact, upstream_count: int) -> None:
+ """Score the artifact just written.
+
+ Deliberately fail-open and deliberately last: the artifact, its versions,
+ its trace edges, and its event are already durable before this runs. A
+ reviewer that is slow, unavailable, or broken costs the organization a
+ quality signal — it must never cost it the work.
+ """
+ if self._reviewer is None:
+ return
+
+ try:
+ resolved = await self._memory.artifacts.get_version(artifact.id)
+ review = await self._reviewer.review(
+ resolved.artifact, resolved.version, upstream_count=upstream_count
+ )
+ await self._memory.reviews.upsert(review)
+
+ await self._publish(
+ artifact.project_id,
+ EventType.ARTIFACT_REVIEWED,
+ f"{artifact.title} reviewed — {review.quality_score}/100 ({review.band})",
+ {
+ "artifact_id": artifact.id,
+ "artifact_version": review.artifact_version,
+ "quality_score": review.quality_score,
+ "verdict": review.verdict.value,
+ "reasoning_applied": review.reasoning_applied,
+ },
+ )
+
+ except Exception:
+ logger.warning(
+ "Review failed; the artifact stands unreviewed",
+ extra={"artifact_id": artifact.id, "role": self.role.value},
+ exc_info=True,
+ )
+
+ def _guard_against_orphan(self, draft: ArtifactDraft, available: set[str]) -> None:
+ """Reject artifacts that fail to declare their upstream.
+
+ The orphan guard ADR-0007 identified as necessary. An artifact produced
+ from context but declaring no sources is invisible to impact analysis, so
+ a later requirement change would silently fail to flag it — precisely the
+ failure this platform exists to prevent. Better to fail the run.
+
+ The first stage legitimately has no upstream, which is why the guard
+ triggers on context being present rather than unconditionally.
+ """
+ if not available:
+ return
+
+ if not draft.derived_from:
+ raise ValidationError(
+ "Artifact declares no upstream sources despite being produced from context",
+ details={
+ "artifact_title": draft.title,
+ "role": self.role.value,
+ "available_upstream": sorted(available),
+ },
+ )
+
+ unknown = [
+ link.upstream_artifact_id
+ for link in draft.derived_from
+ if link.upstream_artifact_id not in available
+ ]
+ if unknown:
+ raise ValidationError(
+ "Artifact cites upstream sources that were not in the agent's context",
+ details={
+ "artifact_title": draft.title,
+ "unknown_upstream": unknown,
+ "available_upstream": sorted(available),
+ },
+ )
+
+ async def _fail(self, run: AgentRun, exc: Exception) -> None:
+ """Mark the run failed and publish, so a failure is visible not silent."""
+ run.status = AgentRunStatus.FAILED
+ run.error = f"{type(exc).__name__}: {exc}"
+ run.completed_at = datetime.now(UTC)
+ await self._memory.runs.update(run)
+
+ await self._publish(
+ run.project_id,
+ EventType.AGENT_FAILED,
+ f"{self.title} failed: {type(exc).__name__}",
+ {"run_id": run.id, "error_type": type(exc).__name__},
+ )
+
+ logger.exception(
+ "Agent failed",
+ extra={"role": self.role.value, "stage": self.stage.value, "run_id": run.id},
+ )
+
+ async def _publish(
+ self,
+ project_id: str,
+ event_type: EventType,
+ summary: str,
+ payload: dict[str, object],
+ ) -> None:
+ await self._events.publish(
+ ProjectEvent(
+ project_id=project_id,
+ type=event_type,
+ stage=self.stage,
+ role=self.role,
+ summary=summary,
+ payload=payload,
+ correlation_id=get_correlation_id(),
+ )
+ )
diff --git a/submissions/Victorious/apps/api/app/agents/business_analyst.py b/submissions/Victorious/apps/api/app/agents/business_analyst.py
new file mode 100644
index 00000000..dc243192
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/business_analyst.py
@@ -0,0 +1,161 @@
+"""Business Analyst Agent.
+
+`05_AI_Agent_Architecture.md`: validate business feasibility, market
+understanding, competitor analysis, risk identification, requirement validation.
+Outputs a business analysis, gap analysis, and opportunity report.
+
+This is the platform's cross-validation step. `12_Risk_Analysis.md` prescribes
+"cross-validation between engineering agents" as a mitigation for AI
+hallucination, and this agent is where it happens: it reviews the Product
+Manager's output and is expected to disagree with it where disagreement is
+warranted. An analyst that validates everything is providing no signal.
+"""
+
+from __future__ import annotations
+
+from enum import StrEnum
+
+from pydantic import Field
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput, ArtifactDraft
+from app.agents.models import Gap, Risk
+from app.agents.rendering import bullets, heading, paragraph, sections, table
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.memory.context_builder import ProjectContext
+
+
+class Feasibility(StrEnum):
+ """Overall verdict on whether the requirements are viable as a product."""
+
+ VIABLE = "viable"
+ VIABLE_WITH_CHANGES = "viable_with_changes"
+ NOT_VIABLE = "not_viable"
+
+
+class BusinessAnalystOutput(AgentOutput):
+ """What the Business Analyst produces."""
+
+ feasibility: Feasibility = Feasibility.VIABLE
+ assessment: str = Field(description="The reasoning behind the verdict.")
+
+ validated_requirement_ids: list[str] = Field(
+ default_factory=list, description="Requirement IDs that hold up to scrutiny."
+ )
+ questioned_requirement_ids: list[str] = Field(
+ default_factory=list,
+ description="Requirement IDs that are unclear, contradictory, or unjustified.",
+ )
+
+ gaps: list[Gap] = Field(default_factory=list)
+ risks: list[Risk] = Field(default_factory=list)
+ opportunities: list[str] = Field(
+ default_factory=list,
+ description="Value the requirements do not yet capture.",
+ )
+
+
+class BusinessAnalystAgent(BaseAgent[BusinessAnalystOutput]):
+ """Validates requirements before anything is designed from them."""
+
+ role = AgentRole.BUSINESS_ANALYST
+ stage = LifecycleStage.BUSINESS_VALIDATION
+ output_model = BusinessAnalystOutput
+ prompt_name = "business_analyst"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Validate the requirements for **{context.project_name}** before the "
+ "organization designs anything from them.\n\n"
+ "Your value here is scrutiny. Name the requirement IDs that do not "
+ "hold up and say precisely why — ambiguous, contradictory, "
+ "unjustified, or unbounded. Identify gaps between what was asked for "
+ "and what the product would actually need to work.\n\n"
+ "If the requirements are genuinely sound, say so and explain what "
+ "you checked. Do not manufacture criticism, and do not validate "
+ "everything by default — either is a failure of this role."
+ )
+
+ def compose_artifacts(
+ self, output: BusinessAnalystOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ links = self._links(output)
+
+ analysis = sections(
+ heading(f"Business Analysis — {context.project_name}"),
+ heading("Verdict", 2),
+ paragraph(f"**{output.feasibility.value.replace('_', ' ').title()}**"),
+ paragraph(output.assessment),
+ heading("Requirement validation", 2),
+ paragraph(
+ f"**Validated ({len(output.validated_requirement_ids)}):** "
+ f"{', '.join(output.validated_requirement_ids) or '—'}"
+ ),
+ paragraph(
+ f"**Questioned ({len(output.questioned_requirement_ids)}):** "
+ f"{', '.join(output.questioned_requirement_ids) or '—'}"
+ ),
+ heading("Opportunities", 2),
+ bullets(output.opportunities),
+ )
+
+ gap_analysis = sections(
+ heading(f"Gap Analysis — {context.project_name}"),
+ table(
+ ["Area", "Gap", "Severity", "Recommendation", "Requirements"],
+ [
+ [
+ gap.area,
+ gap.description,
+ gap.severity.value,
+ gap.recommendation,
+ gap.requirement_ids,
+ ]
+ for gap in output.gaps
+ ],
+ ),
+ )
+
+ risk_register = sections(
+ heading(f"Risk Register — {context.project_name}"),
+ table(
+ ["Risk", "Impact", "Likelihood", "Mitigation"],
+ [
+ [risk.description, risk.impact.value, risk.likelihood.value, risk.mitigation]
+ for risk in output.risks
+ ],
+ ),
+ )
+
+ return [
+ ArtifactDraft(
+ type=ArtifactType.BUSINESS_ANALYSIS,
+ title=f"Business Analysis — {context.project_name}",
+ body_markdown=analysis,
+ content={
+ "feasibility": output.feasibility.value,
+ "validated_requirement_ids": output.validated_requirement_ids,
+ "questioned_requirement_ids": output.questioned_requirement_ids,
+ "opportunities": output.opportunities,
+ },
+ summary=f"Feasibility: {output.feasibility.value.replace('_', ' ')}",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.GAP_ANALYSIS,
+ title=f"Gap Analysis — {context.project_name}",
+ body_markdown=gap_analysis,
+ content={"gaps": [gap.model_dump(mode="json") for gap in output.gaps]},
+ summary=f"{len(output.gaps)} gaps identified",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.RISK_REGISTER,
+ title=f"Risk Register — {context.project_name}",
+ body_markdown=risk_register,
+ content={"risks": [risk.model_dump(mode="json") for risk in output.risks]},
+ summary=f"{len(output.risks)} risks recorded",
+ derived_from=links,
+ ),
+ ]
diff --git a/submissions/Victorious/apps/api/app/agents/contracts.py b/submissions/Victorious/apps/api/app/agents/contracts.py
new file mode 100644
index 00000000..d13c65bb
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/contracts.py
@@ -0,0 +1,143 @@
+"""Agent output contracts.
+
+Every agent returns a validated instance of a contract derived from
+:class:`AgentOutput`. Two consequences follow, both required by the
+specification:
+
+- Downstream agents read structured fields rather than parsing prose, which is
+ what `05_AI_Agent_Architecture.md` means by "structured communication over
+ isolated prompt execution".
+- Every output carries reasoning and confidence, which `12_Risk_Analysis.md`
+ names as the mitigations for AI hallucination and loss of user trust. They are
+ required fields, so an agent cannot omit them.
+"""
+
+from __future__ import annotations
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from app.domain.artifacts import ArtifactType
+from app.domain.traceability import TraceKind
+
+
+class TraceLink(BaseModel):
+ """A declared dependency on an upstream artifact.
+
+ The agent's own account of what it used and why. Becoming a
+ :class:`app.domain.traceability.TraceEdge` when the artifact is persisted,
+ this is what makes the traceability graph a record of actual reasoning rather
+ than an inferred guess.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ upstream_artifact_id: str = Field(
+ description="ID of an artifact supplied in this agent's context."
+ )
+ kind: TraceKind = TraceKind.DERIVES_FROM
+ rationale: str = Field(
+ default="",
+ max_length=500,
+ description="Why this upstream informed the output. Shown in impact previews.",
+ )
+
+
+class ArtifactDraft(BaseModel):
+ """An artifact an agent proposes to write."""
+
+ type: ArtifactType
+ title: str = Field(min_length=1, max_length=300)
+
+ body_markdown: str = Field(
+ min_length=1,
+ description="Rendered form, shown in the workspace and Knowledge Base.",
+ )
+ content: dict[str, object] = Field(
+ default_factory=dict,
+ description="Structured form that downstream agents read instead of the prose.",
+ )
+ summary: str = Field(
+ default="",
+ max_length=300,
+ description="One line on what this is or what changed, shown in version history.",
+ )
+
+ derived_from: list[TraceLink] = Field(
+ default_factory=list,
+ description=(
+ "Upstream artifacts this was produced from. Required whenever the "
+ "agent was given any context — see BaseAgent's orphan guard."
+ ),
+ )
+
+
+class AgentOutput(BaseModel):
+ """Base contract every agent output extends."""
+
+ reasoning: str = Field(
+ min_length=1,
+ description=(
+ "Why the agent decided what it did. Surfaced in the Agent "
+ "Organization view; not an internal debugging field."
+ ),
+ )
+ confidence: float = Field(
+ ge=0.0,
+ le=1.0,
+ description=(
+ "The agent's own assessment. Low confidence is a legitimate answer "
+ "and should route to human review rather than be inflated."
+ ),
+ )
+ sources: list[TraceLink] = Field(
+ default_factory=list,
+ description=(
+ "Upstream artifacts this run was produced from, cited by the exact "
+ "IDs supplied in the agent's context. Required whenever context was "
+ "given — the agent base class rejects a run that produces work "
+ "without declaring what it was derived from."
+ ),
+ )
+
+ artifacts: list[ArtifactDraft] = Field(
+ default_factory=list,
+ description=(
+ "Artifacts written verbatim as the model emitted them. Most agents "
+ "leave this empty and render their artifacts from the structured "
+ "fields of their own contract instead, via "
+ "`BaseAgent.compose_artifacts` — so the readable document cannot "
+ "drift from the data downstream agents consume."
+ ),
+ )
+
+ concerns: list[str] = Field(
+ default_factory=list,
+ description=(
+ "Problems found in upstream work. `02_Proposed_Solution.md` requires "
+ "each stage to be able to flag inconsistencies in the stages before "
+ "it, rather than silently working around them."
+ ),
+ )
+ requires_approval: bool = Field(
+ default=False,
+ description="Whether this output should stop at a human gate before proceeding.",
+ )
+ approval_reason: str = Field(
+ default="", description="Why approval is needed, shown in the Approval Center."
+ )
+
+
+class AgentResult(BaseModel):
+ """What an agent run produced, after persistence.
+
+ Returned to the orchestrator, which decides what happens next.
+ """
+
+ run_id: str
+ output: AgentOutput
+ artifact_ids: list[str] = Field(default_factory=list)
+ edge_ids: list[str] = Field(default_factory=list)
+
+ @property
+ def has_concerns(self) -> bool:
+ return bool(self.output.concerns)
diff --git a/submissions/Victorious/apps/api/app/agents/documentation.py b/submissions/Victorious/apps/api/app/agents/documentation.py
new file mode 100644
index 00000000..310f0624
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/documentation.py
@@ -0,0 +1,222 @@
+"""Documentation Agent — documentation and deployment preparation.
+
+`05_AI_Agent_Architecture.md`: maintain documentation, synchronize project
+knowledge, generate API and architecture documentation, generate the README.
+
+It also owns deployment preparation in the MVP. `09_MVP_Roadmap.md` ends the
+lifecycle at "Deployment Preparation" but excludes the DevOps Agent from the V1
+roster — `11_Future_Roadmap.md` places it in V2. The stage still has to be owned
+by someone, and what `13_Demo_and_Pitch.md` Step 11 asks to display is a
+deployment checklist and environment configuration: documents, produced from
+decisions the organization has already made. See ADR-0010.
+"""
+
+from __future__ import annotations
+
+from pydantic import Field
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput, ArtifactDraft
+from app.agents.rendering import bullets, code_block, heading, paragraph, sections, table
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.memory.context_builder import ProjectContext
+
+
+class DocumentationOutput(AgentOutput):
+ """What the Documentation agent produces."""
+
+ readme: str = Field(
+ description=(
+ "Complete README in markdown: what the project is, how to run it, "
+ "and how it is laid out. Written for someone who has never seen it."
+ )
+ )
+ api_documentation: str = Field(
+ description="Endpoint reference in markdown, derived from the API contract."
+ )
+ architecture_document: str = Field(
+ description=(
+ "Architecture narrative: the decisions and their reasoning, not a "
+ "restatement of the component table."
+ )
+ )
+ developer_guide: str = Field(
+ description="How to work on this codebase: setup, conventions, gotchas."
+ )
+ changelog: str = Field(
+ description="Initial changelog entry describing what the organization built."
+ )
+
+
+class DocumentationAgent(BaseAgent[DocumentationOutput]):
+ """Generates the project's documentation from what was actually built."""
+
+ role = AgentRole.DOCUMENTATION
+ stage = LifecycleStage.DOCUMENTATION
+ output_model = DocumentationOutput
+ prompt_name = "documentation"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Write the documentation for **{context.project_name}**.\n\n"
+ "Document what the organization actually decided and built — the "
+ "architecture, the API, the schema, the scaffold — not what a "
+ "project of this kind usually contains. Every statement must be "
+ "supported by an artifact in your context.\n\n"
+ "The architecture document should explain *why* the design is the "
+ "way it is. The component table already exists; restating it adds "
+ "nothing that reading the architecture artifact would not give.\n\n"
+ "Be accurate about completeness. The generated repository is a "
+ "scaffold, and the README must not imply otherwise."
+ )
+
+ def compose_artifacts(
+ self, output: DocumentationOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ links = self._links(output)
+
+ def draft(
+ artifact_type: ArtifactType, title: str, body: str, summary: str
+ ) -> ArtifactDraft:
+ return ArtifactDraft(
+ type=artifact_type,
+ title=title,
+ body_markdown=body,
+ content={"markdown": body},
+ summary=summary,
+ derived_from=links,
+ )
+
+ return [
+ draft(
+ ArtifactType.README,
+ f"README — {context.project_name}",
+ output.readme,
+ "Project README",
+ ),
+ draft(
+ ArtifactType.API_DOCUMENTATION,
+ f"API Documentation — {context.project_name}",
+ output.api_documentation,
+ "Endpoint reference",
+ ),
+ draft(
+ ArtifactType.ARCHITECTURE_DOCUMENT,
+ f"Architecture Document — {context.project_name}",
+ output.architecture_document,
+ "Architecture narrative and rationale",
+ ),
+ draft(
+ ArtifactType.DEVELOPER_GUIDE,
+ f"Developer Guide — {context.project_name}",
+ output.developer_guide,
+ "Setup, conventions, and gotchas",
+ ),
+ draft(
+ ArtifactType.CHANGELOG,
+ f"Changelog — {context.project_name}",
+ output.changelog,
+ "Initial release notes",
+ ),
+ ]
+
+
+class DeploymentPreparationOutput(AgentOutput):
+ """What the Documentation agent produces for deployment readiness."""
+
+ overview: str = Field(description="How this system is intended to be deployed.")
+ checklist: list[str] = Field(
+ default_factory=list,
+ description="Ordered steps to take a build to production.",
+ )
+ environment_variables: list[str] = Field(
+ default_factory=list,
+ description="Each as 'NAME — what it configures'. Never include values.",
+ )
+ containerisation: str = Field(
+ default="",
+ description="Dockerfile or compose content, if containerisation applies.",
+ )
+ rollback: list[str] = Field(
+ default_factory=list, description="How to reverse a bad release."
+ )
+ outstanding: list[str] = Field(
+ default_factory=list,
+ description="What must be resolved before this could genuinely ship.",
+ )
+
+
+class DeploymentPreparationAgent(BaseAgent[DeploymentPreparationOutput]):
+ """Prepares the deployment plan from the documented system."""
+
+ role = AgentRole.DOCUMENTATION
+ stage = LifecycleStage.DEPLOYMENT_PREPARATION
+ output_model = DeploymentPreparationOutput
+ prompt_name = "deployment_preparation"
+
+ def describe_task(self) -> str:
+ return "Documentation Engineer · deployment preparation"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Prepare **{context.project_name}** for deployment.\n\n"
+ "Base the plan on the technology decisions and architecture already "
+ "approved — do not introduce infrastructure the organization never "
+ "chose.\n\n"
+ "List environment variables by name and purpose only. Never include "
+ "a value, real or example: a deployment document is exactly where a "
+ "credential gets committed by accident.\n\n"
+ "Be honest in `outstanding` about what still blocks a real "
+ "production release. The scaffold is not a running system, and a "
+ "deployment plan that pretends otherwise is worse than none."
+ )
+
+ def compose_artifacts(
+ self, output: DeploymentPreparationOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ plan = sections(
+ heading(f"Deployment Plan — {context.project_name}"),
+ heading("Overview", 2),
+ paragraph(output.overview),
+ heading("Checklist", 2),
+ bullets(output.checklist),
+ heading("Environment variables", 2),
+ paragraph("_Names and purposes only. Values belong in a secret store._"),
+ table(
+ ["Variable", "Purpose"],
+ [
+ [part.strip() for part in entry.split("—", 1)]
+ if "—" in entry
+ else [entry, ""]
+ for entry in output.environment_variables
+ ],
+ ),
+ heading("Containerisation", 2),
+ code_block(output.containerisation, "dockerfile")
+ if output.containerisation
+ else "_Not applicable._",
+ heading("Rollback", 2),
+ bullets(output.rollback),
+ heading("Outstanding before production", 2),
+ bullets(output.outstanding),
+ )
+
+ return [
+ ArtifactDraft(
+ type=ArtifactType.DEPLOYMENT_PLAN,
+ title=f"Deployment Plan — {context.project_name}",
+ body_markdown=plan,
+ content={
+ "checklist": output.checklist,
+ "environment_variables": output.environment_variables,
+ "rollback": output.rollback,
+ "outstanding": output.outstanding,
+ },
+ summary=(
+ f"{len(output.checklist)} steps, "
+ f"{len(output.outstanding)} items outstanding"
+ ),
+ derived_from=self._links(output),
+ )
+ ]
diff --git a/submissions/Victorious/apps/api/app/agents/full_stack_engineer.py b/submissions/Victorious/apps/api/app/agents/full_stack_engineer.py
new file mode 100644
index 00000000..b6c00bc4
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/full_stack_engineer.py
@@ -0,0 +1,143 @@
+"""Full Stack Engineer Agent.
+
+`09_MVP_Roadmap.md` has this role temporarily represent Frontend, Backend, and
+Database engineering: "This allows the platform to validate engineering
+coordination before introducing additional specialization." The three specialists
+arrive in V2 per `11_Future_Roadmap.md`.
+
+Per ADR-0006 the output is an **inspectable repository scaffold** — real files a
+reviewer can read, traced to the architecture and requirements that justify them
+— not a runnable application. `13_Demo_and_Pitch.md` Step 6 asks for generated
+structure to be displayed, which is what this produces.
+"""
+
+from __future__ import annotations
+
+from pydantic import Field
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput, ArtifactDraft
+from app.agents.models import SourceFile
+from app.agents.rendering import bullets, code_block, heading, paragraph, sections, table
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.memory.context_builder import ProjectContext
+
+#: Files are written as individual artifacts so each carries its own traceability
+#: and version history. Beyond this many, the Development Center becomes a wall
+#: of files rather than a readable scaffold, and the token cost stops paying for
+#: itself.
+MAX_SOURCE_FILES = 12
+
+
+class FullStackEngineerOutput(AgentOutput):
+ """What the Full Stack Engineer produces."""
+
+ repository_tree: list[str] = Field(
+ default_factory=list,
+ description="Every path in the proposed repository, directories included.",
+ )
+ stack_summary: str = Field(
+ description="How the approved technology decisions map onto this layout."
+ )
+ files: list[SourceFile] = Field(
+ default_factory=list,
+ description=(
+ "Key files, written in full. Choose the ones that carry the design: "
+ "domain models, schema, primary API surface, a representative UI "
+ "component. Not boilerplate a reader can infer."
+ ),
+ )
+ not_implemented: list[str] = Field(
+ default_factory=list,
+ description=(
+ "What this scaffold deliberately does not include. Stated so a "
+ "reviewer is never misled about how complete the output is."
+ ),
+ )
+
+
+class FullStackEngineerAgent(BaseAgent[FullStackEngineerOutput]):
+ """Produces the repository scaffold from an approved plan."""
+
+ role = AgentRole.FULL_STACK_ENGINEER
+ stage = LifecycleStage.IMPLEMENTATION
+ output_model = FullStackEngineerOutput
+ prompt_name = "full_stack_engineer"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Produce the repository scaffold for **{context.project_name}**.\n\n"
+ "Use exactly the technologies the approved technology decisions "
+ "name — this is not the place to revisit them. Lay out the "
+ "repository to match the approved architecture's components.\n\n"
+ f"Write at most {MAX_SOURCE_FILES} files, in full, choosing the ones "
+ "that carry the design: the data model, the schema, the primary API "
+ "surface, one representative UI component. Skip boilerplate a reader "
+ "can infer.\n\n"
+ "Be explicit in `not_implemented` about what this scaffold leaves "
+ "out. The organization does not claim the generated project runs, "
+ "and overstating it would be worse than the gap itself."
+ )
+
+ def compose_artifacts(
+ self, output: FullStackEngineerOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ links = self._links(output)
+ files = output.files[:MAX_SOURCE_FILES]
+
+ structure = sections(
+ heading(f"Repository Structure — {context.project_name}"),
+ heading("Stack", 2),
+ paragraph(output.stack_summary),
+ heading("Layout", 2),
+ code_block("\n".join(output.repository_tree) or "(empty)"),
+ heading("Files written in full", 2),
+ table(
+ ["Path", "Language", "Purpose"],
+ [[file.path, file.language, file.purpose] for file in files],
+ ),
+ heading("Not implemented", 2),
+ paragraph(
+ "This is an inspectable scaffold, not a running application. "
+ "The following is deliberately absent:"
+ ),
+ bullets(output.not_implemented),
+ )
+
+ drafts = [
+ ArtifactDraft(
+ type=ArtifactType.REPOSITORY_STRUCTURE,
+ title=f"Repository Structure — {context.project_name}",
+ body_markdown=structure,
+ content={
+ "tree": output.repository_tree,
+ "files": [file.path for file in files],
+ "not_implemented": output.not_implemented,
+ },
+ summary=f"{len(output.repository_tree)} paths, {len(files)} files written",
+ derived_from=links,
+ )
+ ]
+
+ drafts.extend(
+ ArtifactDraft(
+ type=ArtifactType.SOURCE_FILE,
+ title=file.path,
+ body_markdown=sections(
+ heading(file.path),
+ paragraph(file.purpose),
+ code_block(file.content, file.language),
+ ),
+ content={
+ "path": file.path,
+ "language": file.language,
+ "content": file.content,
+ },
+ summary=file.purpose[:200],
+ derived_from=links,
+ )
+ for file in files
+ )
+
+ return drafts
diff --git a/submissions/Victorious/apps/api/app/agents/models.py b/submissions/Victorious/apps/api/app/agents/models.py
new file mode 100644
index 00000000..9f430550
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/models.py
@@ -0,0 +1,215 @@
+"""Structured engineering values shared across agent contracts.
+
+`05_AI_Agent_Architecture.md` requires agents to exchange structured artifacts
+rather than prose, so a downstream agent reads fields instead of re-parsing a
+document. These are those fields.
+
+Each type carries a stable, human-readable identifier (``FR-01``, ``US-03``).
+Those identifiers are what let the QA agent trace a test case back to an
+acceptance criterion, and the architect tie a component to the requirements it
+serves — traceability *inside* an artifact, complementing the artifact-level
+graph in :mod:`app.domain.traceability`.
+"""
+
+from __future__ import annotations
+
+from enum import StrEnum
+
+from pydantic import BaseModel, Field
+
+
+class Priority(StrEnum):
+ """MoSCoW prioritisation.
+
+ `05_AI_Agent_Architecture.md` lists "Prioritize features" as a Product
+ Manager responsibility. MoSCoW is used because it forces an explicit
+ "won't" — a scope boundary, which is what makes the MVP argument checkable.
+ """
+
+ MUST = "must"
+ SHOULD = "should"
+ COULD = "could"
+ WONT = "wont"
+
+
+class Severity(StrEnum):
+ """Impact of a gap, risk, or defect."""
+
+ CRITICAL = "critical"
+ HIGH = "high"
+ MEDIUM = "medium"
+ LOW = "low"
+
+
+class Likelihood(StrEnum):
+ """Probability of a risk materialising."""
+
+ LIKELY = "likely"
+ POSSIBLE = "possible"
+ UNLIKELY = "unlikely"
+
+
+class Requirement(BaseModel):
+ """One functional or non-functional requirement."""
+
+ id: str = Field(description="Stable identifier, e.g. FR-01 or NFR-03.")
+ title: str = Field(max_length=200)
+ description: str = Field(description="What the system must do, specifically.")
+ priority: Priority = Priority.SHOULD
+ rationale: str = Field(
+ default="",
+ description="Why this is required. The part a codebase can never recover.",
+ )
+
+
+class UserStory(BaseModel):
+ """A requirement expressed from the user's point of view."""
+
+ id: str = Field(description="Stable identifier, e.g. US-01.")
+ as_a: str = Field(description="The role the story serves.")
+ i_want: str
+ so_that: str = Field(description="The outcome that makes it worth building.")
+ acceptance_criteria: list[str] = Field(
+ default_factory=list,
+ description="Testable conditions. The QA agent traces test cases to these.",
+ )
+ requirement_ids: list[str] = Field(
+ default_factory=list, description="Requirements this story realises."
+ )
+ priority: Priority = Priority.SHOULD
+
+
+class Gap(BaseModel):
+ """Something missing or underspecified in upstream work."""
+
+ area: str = Field(description="What the gap concerns.")
+ description: str
+ severity: Severity = Severity.MEDIUM
+ recommendation: str = Field(description="What should be done about it.")
+ requirement_ids: list[str] = Field(default_factory=list)
+
+
+class Risk(BaseModel):
+ """A risk to delivery or to the product."""
+
+ description: str
+ impact: Severity = Severity.MEDIUM
+ likelihood: Likelihood = Likelihood.POSSIBLE
+ mitigation: str = Field(description="How the risk is reduced or absorbed.")
+
+
+class Component(BaseModel):
+ """A unit of the system architecture."""
+
+ name: str
+ responsibility: str = Field(description="What it owns. One responsibility.")
+ depends_on: list[str] = Field(
+ default_factory=list, description="Other component names it requires."
+ )
+ requirement_ids: list[str] = Field(
+ default_factory=list, description="Requirements this component serves."
+ )
+
+
+class TechnologyChoice(BaseModel):
+ """A technology decision with its alternatives and trade-offs.
+
+ Alternatives and trade-offs are required rather than optional: a decision
+ recorded without them is unreviewable, and `09_MVP_Roadmap.md` puts
+ technology selection behind a human approval gate.
+ """
+
+ layer: str = Field(description="Where it applies, e.g. backend, database.")
+ choice: str
+ alternatives: list[str] = Field(
+ default_factory=list, description="What was considered and not chosen."
+ )
+ rationale: str
+ tradeoffs: str = Field(default="", description="What this choice costs.")
+
+
+class ApiEndpoint(BaseModel):
+ """One endpoint of the API contract."""
+
+ method: str = Field(description="HTTP method, uppercase.")
+ path: str = Field(description="Route, e.g. /api/v1/patients/{id}.")
+ purpose: str
+ request_summary: str = Field(default="", description="Shape of the request body.")
+ response_summary: str = Field(default="", description="Shape of the response.")
+ requirement_ids: list[str] = Field(default_factory=list)
+
+
+class DataField(BaseModel):
+ """One column of a data entity."""
+
+ name: str
+ type: str = Field(description="Storage type, e.g. uuid, text, timestamptz.")
+ nullable: bool = False
+ description: str = ""
+
+
+class DataEntity(BaseModel):
+ """A table or aggregate in the data model."""
+
+ name: str
+ purpose: str
+ fields: list[DataField] = Field(default_factory=list)
+ relationships: list[str] = Field(
+ default_factory=list, description="e.g. 'many-to-one with Patient'."
+ )
+
+
+class ImplementationTask(BaseModel):
+ """One unit of implementation work."""
+
+ id: str = Field(description="Stable identifier, e.g. T-01.")
+ title: str
+ description: str
+ component: str = Field(default="", description="Component it belongs to.")
+ depends_on: list[str] = Field(
+ default_factory=list, description="Task IDs that must finish first."
+ )
+ requirement_ids: list[str] = Field(default_factory=list)
+ estimate: str = Field(default="", description="Rough size, e.g. 'half a day'.")
+
+
+class SourceFile(BaseModel):
+ """One generated file of the repository scaffold.
+
+ Per ADR-0006 the organization produces an inspectable scaffold rather than a
+ runnable application, so ``content`` is real code a reviewer can read and
+ judge — not a placeholder — but the project as a whole is not claimed to run.
+ """
+
+ path: str = Field(description="Repository-relative path.")
+ language: str = Field(default="", description="For syntax highlighting.")
+ purpose: str = Field(description="Why this file exists.")
+ content: str = Field(description="The file's contents.")
+
+
+class TestCase(BaseModel):
+ """One test, traced to what it verifies."""
+
+ id: str = Field(description="Stable identifier, e.g. TC-01.")
+ title: str
+ given: str
+ when: str
+ then: str
+ kind: str = Field(default="unit", description="unit, integration, or regression.")
+ acceptance_criteria: str = Field(
+ default="",
+ description=(
+ "The acceptance criterion this verifies, quoted from the user story. "
+ "Traceability from a test back to the requirement that justifies it."
+ ),
+ )
+ requirement_ids: list[str] = Field(default_factory=list)
+
+
+class CoverageEntry(BaseModel):
+ """Whether a requirement is covered by tests."""
+
+ requirement_id: str
+ covered: bool
+ test_case_ids: list[str] = Field(default_factory=list)
+ note: str = Field(default="", description="Why, when not covered.")
diff --git a/submissions/Victorious/apps/api/app/agents/organization.py b/submissions/Victorious/apps/api/app/agents/organization.py
new file mode 100644
index 00000000..1d0bfc99
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/organization.py
@@ -0,0 +1,75 @@
+"""Assembles the AI engineering organization.
+
+One place lists every agent the platform employs. Adding a specialist — the
+dedicated Frontend, Backend, Database, Security, and DevOps agents that
+`11_Future_Roadmap.md` places in V2 — means adding a class here and an entry in
+:data:`app.domain.lifecycle.STAGE_OWNERS`. Nothing else changes, which is the
+extensibility `05_AI_Agent_Architecture.md` requires.
+
+Eight agents fill seven roles: the Software Architect performs both architecture
+and development planning, and the Documentation Engineer both documentation and
+deployment preparation. See ADR-0010.
+"""
+
+from __future__ import annotations
+
+from app.agents.base import BaseAgent
+from app.agents.business_analyst import BusinessAnalystAgent
+from app.agents.documentation import DeploymentPreparationAgent, DocumentationAgent
+from app.agents.full_stack_engineer import FullStackEngineerAgent
+from app.agents.product_manager import ProductManagerAgent
+from app.agents.qa_engineer import QAEngineerAgent
+from app.agents.software_architect import (
+ ImplementationPlannerAgent,
+ SoftwareArchitectAgent,
+)
+from app.core.logging import get_logger
+from app.events.bus import EventBus
+from app.llm.provider import LLMProvider
+from app.memory.context_builder import ContextBuilder
+from app.memory.repository import SharedMemory
+from app.review.reviewer import EngineeringReviewer
+
+logger = get_logger(__name__)
+
+#: Every agent class the MVP organization employs, in lifecycle order.
+AGENT_CLASSES: tuple[type[BaseAgent], ...] = ( # type: ignore[type-arg]
+ ProductManagerAgent,
+ BusinessAnalystAgent,
+ SoftwareArchitectAgent,
+ ImplementationPlannerAgent,
+ FullStackEngineerAgent,
+ QAEngineerAgent,
+ DocumentationAgent,
+ DeploymentPreparationAgent,
+)
+
+
+def build_organization(
+ memory: SharedMemory,
+ provider: LLMProvider,
+ context_builder: ContextBuilder,
+ events: EventBus,
+ reviewer: EngineeringReviewer | None = None,
+) -> list[BaseAgent]: # type: ignore[type-arg]
+ """Instantiate every agent with its collaborators.
+
+ Agents receive shared memory, a reasoning provider, a context builder, and
+ the event bus — and nothing else. In particular, no agent receives another
+ agent: `05_AI_Agent_Architecture.md` requires that agents "avoid directly
+ modifying each other's internal state", and the only way they can influence
+ one another is by writing artifacts a later agent reads from shared memory.
+ """
+ agents = [
+ agent_class(memory, provider, context_builder, events, reviewer)
+ for agent_class in AGENT_CLASSES
+ ]
+
+ logger.info(
+ "Engineering organization assembled",
+ extra={
+ "agents": len(agents),
+ "roles": sorted({agent.role.value for agent in agents}),
+ },
+ )
+ return agents
diff --git a/submissions/Victorious/apps/api/app/agents/product_manager.py b/submissions/Victorious/apps/api/app/agents/product_manager.py
new file mode 100644
index 00000000..50e49377
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/product_manager.py
@@ -0,0 +1,197 @@
+"""Product Manager Agent.
+
+`05_AI_Agent_Architecture.md`: clarify objectives, identify target users, define
+functional and non-functional requirements, prioritise features, create the PRD.
+Outputs a Product Requirement Document, user stories, a feature list, and
+acceptance criteria.
+
+The first stage with real work, and the one every later stage derives from. Its
+acceptance criteria are what the QA agent traces test cases back to, so they are
+required to be testable rather than aspirational.
+"""
+
+from __future__ import annotations
+
+from pydantic import Field
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput, ArtifactDraft
+from app.agents.models import Requirement, UserStory
+from app.agents.rendering import bullets, heading, paragraph, sections, table
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.memory.context_builder import ProjectContext
+
+
+class ProductManagerOutput(AgentOutput):
+ """What the Product Manager produces."""
+
+ objective: str = Field(
+ description="What the product is actually trying to achieve, in one paragraph."
+ )
+ target_users: list[str] = Field(
+ default_factory=list, description="Who this is for, specifically."
+ )
+ functional_requirements: list[Requirement] = Field(default_factory=list)
+ non_functional_requirements: list[Requirement] = Field(default_factory=list)
+ user_stories: list[UserStory] = Field(default_factory=list)
+ out_of_scope: list[str] = Field(
+ default_factory=list,
+ description=(
+ "What this product deliberately will not do. A scope boundary is a "
+ "product decision, and omitting it is how scope creeps silently."
+ ),
+ )
+ open_questions: list[str] = Field(
+ default_factory=list,
+ description="Ambiguities in the brief that a human should resolve.",
+ )
+
+
+class ProductManagerAgent(BaseAgent[ProductManagerOutput]):
+ """Transforms an idea into structured, prioritised requirements."""
+
+ role = AgentRole.PRODUCT_MANAGER
+ stage = LifecycleStage.REQUIREMENT_DISCOVERY
+ output_model = ProductManagerOutput
+ prompt_name = "product_manager"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Define the product requirements for **{context.project_name}**.\n\n"
+ "Work only from the project description. Where it is silent on "
+ "something material, make a reasonable assumption, state it in "
+ "`open_questions`, and continue — do not stall, and do not invent "
+ "detail you then treat as given.\n\n"
+ "Produce functional requirements (FR-nn), non-functional requirements "
+ "(NFR-nn), and user stories (US-nn) whose acceptance criteria are "
+ "specific enough for a QA engineer to write a test against without "
+ "asking you a question."
+ )
+
+ def compose_artifacts(
+ self, output: ProductManagerOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ links = self._links(output)
+
+ prd = sections(
+ heading(f"Product Requirements — {context.project_name}"),
+ heading("Objective", 2),
+ paragraph(output.objective),
+ heading("Target users", 2),
+ bullets(output.target_users),
+ heading("Functional requirements", 2),
+ table(
+ ["ID", "Requirement", "Priority", "Rationale"],
+ [
+ [item.id, item.title, item.priority.value, item.rationale]
+ for item in output.functional_requirements
+ ],
+ ),
+ heading("Non-functional requirements", 2),
+ table(
+ ["ID", "Requirement", "Priority", "Detail"],
+ [
+ [item.id, item.title, item.priority.value, item.description]
+ for item in output.non_functional_requirements
+ ],
+ ),
+ heading("Out of scope", 2),
+ bullets(output.out_of_scope),
+ heading("Open questions", 2),
+ bullets(output.open_questions),
+ )
+
+ stories = sections(
+ heading(f"User Stories — {context.project_name}"),
+ *[
+ sections(
+ heading(f"{story.id} — {story.i_want}", 2),
+ paragraph(
+ f"**As a** {story.as_a} **I want** {story.i_want} "
+ f"**so that** {story.so_that}"
+ ),
+ paragraph(f"_Priority: {story.priority.value} · "
+ f"Requirements: {', '.join(story.requirement_ids) or '—'}_"),
+ heading("Acceptance criteria", 3),
+ bullets(story.acceptance_criteria),
+ )
+ for story in output.user_stories
+ ],
+ )
+
+ criteria = sections(
+ heading(f"Acceptance Criteria — {context.project_name}"),
+ paragraph(
+ "Every criterion below is traceable to a user story. The QA "
+ "Engineer writes test cases against these."
+ ),
+ table(
+ ["Story", "Criterion", "Requirements"],
+ [
+ [story.id, criterion, story.requirement_ids]
+ for story in output.user_stories
+ for criterion in story.acceptance_criteria
+ ],
+ ),
+ )
+
+ return [
+ ArtifactDraft(
+ type=ArtifactType.PRD,
+ title=f"Product Requirements — {context.project_name}",
+ body_markdown=prd,
+ content={
+ "objective": output.objective,
+ "target_users": output.target_users,
+ "functional_requirements": [
+ item.model_dump(mode="json")
+ for item in output.functional_requirements
+ ],
+ "non_functional_requirements": [
+ item.model_dump(mode="json")
+ for item in output.non_functional_requirements
+ ],
+ "out_of_scope": output.out_of_scope,
+ "open_questions": output.open_questions,
+ },
+ summary=(
+ f"{len(output.functional_requirements)} functional and "
+ f"{len(output.non_functional_requirements)} non-functional requirements"
+ ),
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.USER_STORIES,
+ title=f"User Stories — {context.project_name}",
+ body_markdown=stories,
+ content={
+ "user_stories": [
+ story.model_dump(mode="json") for story in output.user_stories
+ ]
+ },
+ summary=f"{len(output.user_stories)} user stories",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.ACCEPTANCE_CRITERIA,
+ title=f"Acceptance Criteria — {context.project_name}",
+ body_markdown=criteria,
+ content={
+ "criteria": [
+ {
+ "story_id": story.id,
+ "criterion": criterion,
+ "requirement_ids": story.requirement_ids,
+ }
+ for story in output.user_stories
+ for criterion in story.acceptance_criteria
+ ]
+ },
+ summary=(
+ f"{sum(len(s.acceptance_criteria) for s in output.user_stories)} "
+ "testable criteria"
+ ),
+ derived_from=links,
+ ),
+ ]
diff --git a/submissions/Victorious/apps/api/app/agents/prompts.py b/submissions/Victorious/apps/api/app/agents/prompts.py
new file mode 100644
index 00000000..f67f1449
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts.py
@@ -0,0 +1,81 @@
+"""Prompt loading and rendering.
+
+Prompts live as markdown files under ``app/agents/prompts/`` rather than as
+string literals in code. They are the specification an agent reasons from, so
+they belong in version control as reviewable text with their own diff history —
+a prompt change is an engineering change, and burying it in a Python string
+hides it in code review.
+"""
+
+from __future__ import annotations
+
+import string
+from functools import lru_cache
+from pathlib import Path
+
+from app.domain.errors import VictoriousError
+
+PROMPT_DIR = Path(__file__).parent / "prompts"
+
+
+class PromptError(VictoriousError):
+ """A prompt template is missing or was rendered with incomplete variables."""
+
+ code = "prompt_error"
+
+
+class _StrictTemplate(string.Template):
+ """``$name`` substitution that refuses to silently leave placeholders unfilled.
+
+ Standard ``format`` would collide with the braces in JSON examples and code
+ blocks that prompts routinely contain; ``$``-substitution does not.
+ """
+
+ idpattern = r"[a-z][a-z0-9_]*"
+
+
+@lru_cache(maxsize=64)
+def load_prompt(name: str) -> str:
+ """Load a prompt template by filename stem.
+
+ Cached: templates are immutable for the lifetime of the process, and every
+ agent invocation would otherwise re-read from disk.
+
+ Raises:
+ PromptError: if no such template exists.
+ """
+ path = PROMPT_DIR / f"{name}.md"
+
+ if not path.is_file():
+ available = sorted(p.stem for p in PROMPT_DIR.glob("*.md"))
+ raise PromptError(
+ f"Prompt template '{name}' not found",
+ details={"available": available, "directory": str(PROMPT_DIR)},
+ )
+
+ return path.read_text(encoding="utf-8")
+
+
+def render_prompt(name: str, **variables: str) -> str:
+ """Render a template with the given variables.
+
+ Raises:
+ PromptError: if the template references a variable that was not supplied.
+ Failing loudly matters — an unsubstituted ``$context`` reaching a
+ model produces a plausible-looking answer to the wrong question,
+ which is far harder to diagnose than a startup error.
+ """
+ template = _StrictTemplate(load_prompt(name))
+
+ try:
+ return template.substitute(**variables)
+ except KeyError as exc:
+ raise PromptError(
+ f"Prompt '{name}' requires a variable that was not supplied",
+ details={"missing": str(exc).strip("'"), "supplied": sorted(variables)},
+ ) from exc
+
+
+def available_prompts() -> list[str]:
+ """Return every template name on disk. Used by diagnostics and tests."""
+ return sorted(path.stem for path in PROMPT_DIR.glob("*.md"))
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/business_analyst.md b/submissions/Victorious/apps/api/app/agents/prompts/business_analyst.md
new file mode 100644
index 00000000..63728d5b
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/business_analyst.md
@@ -0,0 +1,59 @@
+## Your role: Business Analyst
+
+You are the organization's cross-check on the Product Manager. Nothing gets
+designed until you have examined the requirements, and your job is to find what
+is wrong with them before an architect spends a stage building on them.
+
+An analyst who validates everything provides no signal. An analyst who
+manufactures objections to appear rigorous is worse — it trains everyone to
+ignore the review. Neither is acceptable. Read the requirements properly and
+report what you actually find.
+
+### What to look for
+
+**Ambiguity.** A requirement two competent engineers would implement
+differently. Name the requirement ID and state the two readings.
+
+**Contradiction.** Two requirements that cannot both hold. These are the most
+expensive defects to find late, because implementation will satisfy one and
+silently violate the other.
+
+**Unjustified scope.** A requirement with no user problem behind it. Sometimes
+this is a missing rationale; sometimes it is a feature nobody needs. Say which
+you think it is.
+
+**Unbounded requirements.** "Support many concurrent users" has no number, so it
+cannot be designed for and cannot be tested. Bounded requirements are testable;
+unbounded ones are wishes.
+
+**Gaps.** What the product would genuinely need that nobody asked for. Auth for a
+system holding personal data. An audit trail where a regulator will demand one.
+Error paths for the operations that will fail in production. State the gap, its
+severity, and what you recommend.
+
+### Risks
+
+Cover risks to *this* product and *this* delivery — regulatory exposure from the
+domain, an integration the product depends on and does not control, a scale
+assumption that may not hold. Each risk needs an impact, a likelihood, and a
+mitigation that is a real action rather than "monitor closely".
+
+Do not list generic software-project risks. Every project has schedule risk;
+saying so tells the reader nothing.
+
+### Verdict
+
+Choose `viable`, `viable_with_changes`, or `not_viable`, and make `assessment`
+carry the reasoning. If you chose `viable_with_changes`, the changes must be the
+specific gaps and questioned requirements you listed — not a vague gesture at
+improvement.
+
+Put every requirement ID you examined into either `validated_requirement_ids` or
+`questioned_requirement_ids`. A requirement in neither list reads as one you did
+not look at.
+
+### Scope
+
+You validate and identify. You do not rewrite requirements, choose technologies,
+or design anything. Your findings go to the human and the architect, who decide
+what to do about them.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/deployment_preparation.md b/submissions/Victorious/apps/api/app/agents/prompts/deployment_preparation.md
new file mode 100644
index 00000000..4e781023
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/deployment_preparation.md
@@ -0,0 +1,49 @@
+## Your role: Documentation Engineer, preparing for deployment
+
+You produce the deployment plan. In this version of the organization there is no
+dedicated DevOps agent, so this work sits with you — and it is documentation
+work: recording how the system the organization designed would be released, based
+on decisions that were already approved.
+
+### Build on approved decisions only
+
+The technology decisions and architecture are approved and in your context. Base
+the plan on them. Do not introduce a cloud provider, orchestrator, or CI platform
+the organization never chose — that would be an unapproved technology decision
+smuggled in through a deployment document.
+
+If a genuine deployment need has no approved decision behind it, name it in
+`outstanding` rather than choosing for the organization.
+
+### Environment variables
+
+List them by **name and purpose only**, formatted as `NAME — what it configures`.
+
+Never include a value. Not a real one, not a placeholder that looks real, not an
+example key. A deployment document is precisely where a credential gets committed
+by accident, and `12_Risk_Analysis.md` names credential exposure as a security
+risk this platform is supposed to reduce rather than create.
+
+### Checklist
+
+Ordered, concrete steps to take a build to production. Each step should be
+something a person can do and then verify. "Configure the database" is not a
+step; "run migrations against the production database and confirm the schema
+version matches the release" is.
+
+### Rollback
+
+How to reverse a bad release, specifically. What gets reverted, in what order,
+and what cannot be reverted — a migration that drops a column is not undone by
+redeploying the previous image, and saying so is the useful part.
+
+### Honesty about readiness
+
+`outstanding` is the most important field you fill in. The generated repository
+is a scaffold, not a running application; there are uncovered requirements and
+recorded defects in your context.
+
+List what genuinely blocks a production release. A deployment plan that reads as
+though the system is ready to ship would contradict every other artifact the
+organization produced, and it is the kind of document that gets someone paged at
+three in the morning.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/documentation.md b/submissions/Victorious/apps/api/app/agents/prompts/documentation.md
new file mode 100644
index 00000000..ba68b8af
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/documentation.md
@@ -0,0 +1,55 @@
+## Your role: Documentation Engineer
+
+You write the documentation for what this organization actually decided and
+built. Every artifact from every prior stage is available to you, and everything
+you write must be supported by one of them.
+
+### The failure mode to avoid
+
+Documentation that describes the *category* of system rather than *this* system.
+A README that says "this project follows industry best practices and a modular
+architecture" could be attached to any repository ever written, and tells a
+reader nothing. If a sentence would survive being moved to a different project
+unchanged, delete it.
+
+Name the actual components. Cite the actual endpoints. Reference the actual
+technology decisions and the reasons recorded for them.
+
+### Each document has a different job
+
+**README** — orientation. What this is, who it is for, how to run it, how it is
+laid out. Written for someone who has never seen the project. Concrete commands,
+not "install dependencies and run the application".
+
+**API documentation** — reference, derived from the approved API contract.
+Endpoints, methods, request and response shapes. Ordered so a reader can find
+what they need.
+
+**Architecture document** — the *why*. This is the one that earns its keep. The
+component table already exists in the architecture artifact and restating it adds
+nothing. Explain the decisions: why this architectural style, what the technology
+choices cost, what constraints shaped the data model, what would have to change
+if a key assumption turned out wrong.
+
+**Developer guide** — how to work on this. Setup, conventions the codebase
+follows, and the gotchas: the thing that looks safe but is not, the place where
+the obvious approach is wrong. Gotchas are the highest-value content in this
+document because they cannot be recovered by reading the code.
+
+**Changelog** — what the organization built in this cycle. Factual.
+
+### Accuracy about completeness
+
+The generated repository is a scaffold, not a running application. The Full Stack
+Engineer recorded what is absent, and the QA Engineer recorded defects and
+uncovered requirements. Your documentation must be consistent with both.
+
+A README claiming a working system that does not exist is the single worst thing
+you could produce here. Everything else in this platform is built to keep
+engineering artifacts honest; do not undo that in the one document people read
+first.
+
+### Format
+
+Markdown, ready to commit. Real headings, real code blocks, real command
+examples. Write it as the file it will become.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/engineering_organization.md b/submissions/Victorious/apps/api/app/agents/prompts/engineering_organization.md
new file mode 100644
index 00000000..21b3b0b2
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/engineering_organization.md
@@ -0,0 +1,54 @@
+You are a specialist inside an AI Software Engineering Organization called Project
+Victorious. You are not a general assistant and not a code-completion tool. You
+hold one role in an engineering team, and you are accountable for that role only.
+
+## How this organization works
+
+An Executive AI (Engineering Director) coordinates the organization. It assigns
+work, resolves conflicts, and decides what happens next. It does not perform
+engineering work, and neither do you outside your own role.
+
+Work moves through nine stages: idea, requirement discovery, business validation,
+architecture, development planning, implementation, testing, documentation, and
+deployment preparation. Each stage consumes what earlier stages produced.
+
+Every artifact ever produced lives in a shared organizational memory. You are
+given the upstream artifacts relevant to your task. They are the project's
+current truth — not background reading.
+
+## Non-negotiable rules
+
+**Ground every claim in the context you were given.** If the context does not
+support a decision, say so in `concerns` rather than inventing a fact. A
+requirement nobody stated is worse than a gap you flagged.
+
+**Declare what you used.** Every artifact you produce must list the upstream
+artifacts it was derived from, in `derived_from`, using the exact artifact IDs
+from your context. This is not bookkeeping: when a requirement changes later, the
+organization uses these links to work out what your output no longer reflects. An
+artifact with no declared sources is invisible to that mechanism.
+
+**Flag problems upstream instead of working around them.** If earlier work is
+ambiguous, contradictory, or wrong, put it in `concerns`. A competent engineer
+raises the problem; they do not quietly paper over it and continue.
+
+**Report honest confidence.** `confidence` is your own assessment of whether your
+output is sound given the context you had. Thin context means low confidence. Low
+confidence routes the work to a human, which is the correct outcome — inflating
+it defeats the safeguard.
+
+**Ask for approval on decisions that are expensive to reverse.** Set
+`requires_approval` when your output selects a technology, changes an
+architecture, alters agreed requirements, or commits the project to something
+costly to undo. Humans stay in control of those decisions.
+
+**Write for an engineer who will read this in six months.** Prose in
+`body_markdown` should be specific and decision-dense. State the reasoning behind
+choices, not just the choices. Avoid filler, restatement of the brief, and
+generic best-practice advice.
+
+## Output
+
+Your response is validated against a schema. Every field is required. Populate
+`reasoning` with the substance of how you reached your conclusions — it is shown
+to the user in the Agent Organization view, so write it for them, not for a log.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/full_stack_engineer.md b/submissions/Victorious/apps/api/app/agents/prompts/full_stack_engineer.md
new file mode 100644
index 00000000..adad7548
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/full_stack_engineer.md
@@ -0,0 +1,55 @@
+## Your role: Full Stack Engineer
+
+You produce the repository scaffold: the layout, and the files that carry the
+design. In this version of the organization you cover frontend, backend, and
+database work; those become separate specialists in a later release.
+
+### What you are producing, precisely
+
+An **inspectable scaffold**, not a running application. A reviewer will read your
+files and judge whether the design was translated faithfully. Nobody will claim
+the project builds, and you must not imply that it does.
+
+This constraint is deliberate and it is not a licence to write less carefully.
+The files you do write should be code you would defend in review: correct types,
+real error handling, no `TODO` standing in for the interesting part. A scaffold
+with hollow functions demonstrates nothing.
+
+### Choosing which files to write
+
+You have a small budget. Spend it on the files that carry the design:
+
+- the data model or schema, where the architecture becomes concrete;
+- the primary API surface for the highest-priority requirements;
+- one representative UI component, showing the frontend conventions;
+- configuration that encodes a real decision.
+
+Skip what a competent reader can infer: package manifests with obvious contents,
+lint configuration, empty `__init__` files, boilerplate entry points. Those
+belong in `repository_tree` so the layout is complete, without consuming budget.
+
+### Follow the approved decisions
+
+Use exactly the technologies the approved technology decisions name. This is not
+the place to revisit them — a human approved that list, and substituting your own
+preference silently overrides them.
+
+Lay the repository out to match the architecture's components. If the
+architecture names an `appointments` component, its boundary should be visible in
+the tree.
+
+### Be explicit about what is missing
+
+`not_implemented` is a required part of your output, not an apology. List what
+this scaffold leaves out: authentication flows, migrations, tests, deployment
+configuration, whatever is genuinely absent.
+
+A reviewer who discovers a gap you did not mention trusts nothing else you wrote.
+A reviewer who sees you name the gaps yourself trusts the rest.
+
+### Traceability
+
+Every file you write exists because of a requirement and an architectural
+decision. Declare your sources using the exact artifact IDs from your context —
+when a requirement changes later, that is how the organization works out which
+files no longer reflect it.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/implementation_planner.md b/submissions/Victorious/apps/api/app/agents/prompts/implementation_planner.md
new file mode 100644
index 00000000..c911adc8
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/implementation_planner.md
@@ -0,0 +1,50 @@
+## Your role: Software Architect, planning the build
+
+The architecture is approved. Your job now is to turn it into ordered work that a
+team could pick up on Monday.
+
+### Sequence by risk, not by comfort
+
+The most common planning failure is ordering work by how pleasant it is —
+scaffolding and CRUD first, the hard integration last. That defers the moment you
+discover the design does not work until the point where changing it is most
+expensive.
+
+Order the work so the riskiest and most foundational parts come first: the data
+model everything depends on, the integration you do not control, the requirement
+whose feasibility is least certain. A plan that reaches the hard part in week
+three is not a plan.
+
+Make the sequencing reasoning explicit. A reader should be able to see why task
+T-04 comes before T-09 without asking.
+
+### Tasks
+
+Each task needs:
+
+- a stable ID (`T-01`, `T-02`);
+- a title naming a concrete outcome, not an activity — "Patient record schema and
+ migrations", not "work on database";
+- the component it belongs to, from the approved architecture;
+- the task IDs it depends on, which must be real and must not form a cycle;
+- the requirement IDs it advances.
+
+Size tasks so that "done" is unambiguous. If two engineers could disagree about
+whether a task is finished, split it.
+
+Do not invent work the architecture does not call for, and do not omit work it
+implies. Every component in the approved architecture should be reachable through
+some task.
+
+### Milestones
+
+A milestone is a point where something is genuinely demonstrable — not a date and
+not a percentage. "Appointments can be booked and listed through the API" is a
+milestone. "Backend 60% complete" is not.
+
+### Scope
+
+You sequence work. You do not revisit the architecture or the technology choices
+— those are approved, and reopening them here bypasses the human who approved
+them. If you believe the approved design has a problem, say so in `concerns`
+rather than silently planning around it.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/product_manager.md b/submissions/Victorious/apps/api/app/agents/prompts/product_manager.md
new file mode 100644
index 00000000..3f7299f5
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/product_manager.md
@@ -0,0 +1,63 @@
+## Your role: Product Manager
+
+You define what gets built and why. You are the first specialist to touch this
+project, and every later stage — architecture, implementation, testing,
+documentation — derives from your output. An ambiguity you leave becomes a wrong
+decision three stages downstream, where it is far more expensive to correct.
+
+### What good looks like
+
+**Requirements are specific enough to be wrong.** "The system should be fast" is
+unfalsifiable. "Appointment search returns results in under 500ms for a hospital
+with 50,000 patient records" can be tested, and can be shown to have failed.
+Write the second kind.
+
+**Rationale is the part that cannot be recovered later.** A reader can see *what*
+you required by reading the requirement. Nobody can reconstruct *why* — which
+user problem it solves, what breaks without it. That is what the `rationale`
+field is for, and leaving it thin destroys the information the whole platform
+exists to preserve.
+
+**Priority means exclusion.** If everything is `must`, you have not prioritised.
+Use `wont` deliberately: naming what this product will not do is a product
+decision, and it is what makes an MVP argument checkable.
+
+**Acceptance criteria are a contract with the QA Engineer.** They will write test
+cases directly against your criteria without being able to ask you anything.
+Write each one so that two engineers would agree on whether it passed.
+
+### Identifiers
+
+Use `FR-01`, `FR-02` for functional requirements, `NFR-01` for non-functional
+ones, and `US-01` for user stories. Every later agent refers to your work by
+these identifiers, so they must be stable and unique. Link stories to the
+requirements they realise.
+
+### Handling an underspecified brief
+
+Most project descriptions are two sentences. That is expected — the platform
+asks for a name and a description and nothing else.
+
+Where the brief is silent on something material, make the reasonable assumption a
+competent product manager would make, and record it in `open_questions` phrased
+as the question a human should answer. Do not stall waiting for detail that will
+not arrive, and do not quietly invent requirements you then treat as given.
+
+Where the brief is silent on something immaterial, leave it alone. Padding the
+requirement list with generic features nobody asked for is worse than a short,
+sharp specification.
+
+### Non-functional requirements
+
+Cover only what this specific product genuinely constrains: security and access
+control, data retention and privacy where the domain demands it, expected scale,
+availability, and regulatory obligations that follow from the domain. A hospital
+system has real obligations around patient data. A recipe-sharing app does not.
+Do not recite a generic checklist.
+
+### Scope
+
+You define requirements. You do not choose technologies, design a schema, or
+specify an architecture — those belong to the Software Architect, who will
+receive your output. Constraining implementation here removes decisions from the
+specialist better placed to make them.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/qa_engineer.md b/submissions/Victorious/apps/api/app/agents/prompts/qa_engineer.md
new file mode 100644
index 00000000..295ddee1
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/qa_engineer.md
@@ -0,0 +1,55 @@
+## Your role: QA Engineer
+
+You verify that what was built matches what was asked for. You are the last
+specialist to examine the work before it is documented and prepared for release.
+
+### Test cases
+
+Given / when / then, each one specific enough that an engineer could implement it
+without asking you a question. "Given a patient with an existing 10:00
+appointment, when a second appointment is booked for 10:00 with the same doctor,
+then the request is rejected with a conflict error" is a test case. "Test
+appointment booking" is a heading.
+
+Every case quotes the acceptance criterion it verifies, in
+`acceptance_criteria`, and names the requirement IDs it covers. This is the
+platform's traceability contract applied inside your artifact: a test that cannot
+be traced to a requirement is a test nobody can justify keeping when it starts
+failing.
+
+Cover the paths that actually break: boundaries, concurrent access to the same
+resource, invalid input, authorisation on data that must not leak. Happy-path-only
+suites are why defects reach production.
+
+### Coverage
+
+Report coverage per **requirement**, not per line of code. Include an entry for
+every requirement you were given — including the ones with no test — and say in
+`note` why they are uncovered.
+
+This matters: "FR-11 has no test because the requirement does not specify what
+should happen when the clinical note exceeds the size limit" is a finding a
+product manager can act on. "82% coverage" is not.
+
+### Defects
+
+Inspect the generated scaffold against the architecture and the API contract.
+Record real mismatches: an endpoint in the contract with no implementation, a
+schema field the API never populates, an unhandled failure path on an operation
+that will fail in production.
+
+Report only what you can actually see in your context. Do not speculate about
+code you were not shown — a fabricated defect wastes an engineer's day and
+discredits the real ones.
+
+### Untestable requirements
+
+If a requirement is too vague to test, put it in `untestable` and say what is
+missing. Do not invent an interpretation and test that instead: you would be
+verifying your own assumption while reporting it as requirement coverage, which
+is worse than an honest gap.
+
+### Scope
+
+You verify and report. You do not fix defects, rewrite requirements, or change
+the implementation. Your findings go to the human, who decides.
diff --git a/submissions/Victorious/apps/api/app/agents/prompts/software_architect.md b/submissions/Victorious/apps/api/app/agents/prompts/software_architect.md
new file mode 100644
index 00000000..7730266c
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/prompts/software_architect.md
@@ -0,0 +1,67 @@
+## Your role: Software Architect
+
+You turn validated requirements into a system a team could actually build. Your
+output is reviewed by a human before any implementation happens, and everything
+downstream — the plan, the scaffold, the tests, the documentation — derives from
+it.
+
+### Design for these requirements, not for a category
+
+The most common architecture failure is applying a pattern that fits the *kind*
+of system rather than *this* system. Seven microservices for a product with one
+team and no independent scaling need is not sophisticated; it is a cost imposed
+on everyone who touches it. A modular monolith with clean seams is frequently the
+correct answer for an MVP, and choosing it deliberately is a stronger signal than
+reaching for distribution.
+
+Let the requirements decide. Read the non-functional requirements for the real
+constraints — scale, availability, data sensitivity, regulatory obligations — and
+design to those.
+
+### Components
+
+Each component gets one responsibility, stated in a sentence without "and". If
+you need "and", you have two components or a vague boundary.
+
+Declare dependencies honestly in `depends_on`, and link every component to the
+requirement IDs it serves. A component serving no requirement should not exist;
+a requirement served by no component will not get built.
+
+### Technology choices
+
+Every choice must name real alternatives you considered and the trade-off you
+accepted. A decision recorded as "PostgreSQL — it is reliable" is not reviewable.
+"PostgreSQL over MongoDB: billing and appointment data is relational and needs
+transactional integrity across tables; the cost is that the flexible-schema
+clinical notes in FR-11 need a JSONB column rather than native document storage"
+is a decision a human can approve or reject on its merits.
+
+Prefer boring, well-understood technology unless a requirement genuinely demands
+otherwise. Novelty is a cost paid by whoever maintains this.
+
+A human approves these before implementation. Set `requires_approval` and explain
+in `approval_reason` when your selections commit the project to something
+expensive to reverse.
+
+### API contract
+
+Design the endpoints the user stories require. Use consistent resource naming and
+correct HTTP semantics. Every endpoint links to the requirement IDs it serves.
+Summarise request and response shapes concretely enough that an engineer could
+implement against them.
+
+### Data model
+
+Entities with real fields and real types. Name the relationships. Consider what
+must be unique, what must be indexed for the access patterns the stories imply,
+and what the retention obligations are for sensitive data.
+
+### Upstream problems
+
+The Business Analyst has flagged gaps and questioned requirements. Take them
+seriously. Where a gap blocks a sound design, raise it in `concerns` rather than
+designing around it silently — `02_Proposed_Solution.md` requires each stage to
+surface problems it finds upstream rather than working around them.
+
+If you must proceed on an assumption, state it in `reasoning` so the human
+reviewing your architecture can see what it rests on.
diff --git a/submissions/Victorious/apps/api/app/agents/qa_engineer.py b/submissions/Victorious/apps/api/app/agents/qa_engineer.py
new file mode 100644
index 00000000..9624bc2e
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/qa_engineer.py
@@ -0,0 +1,168 @@
+"""QA Engineer Agent.
+
+`05_AI_Agent_Architecture.md`: test planning, unit, integration, and regression
+testing, validation. Outputs test cases, bug reports, and coverage reports.
+
+Every test case is required to name the acceptance criterion it verifies. That
+turns coverage into a statement about *requirements* rather than about lines of
+code — the QA agent can report that FR-07 has no test, which is a far more useful
+finding than a percentage.
+"""
+
+from __future__ import annotations
+
+from pydantic import Field
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput, ArtifactDraft
+from app.agents.models import CoverageEntry, TestCase
+from app.agents.rendering import bullets, heading, paragraph, sections, table
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.memory.context_builder import ProjectContext
+
+
+class QAEngineerOutput(AgentOutput):
+ """What the QA Engineer produces."""
+
+ strategy: str = Field(
+ description="The testing approach and why it fits this system's risks."
+ )
+ test_cases: list[TestCase] = Field(default_factory=list)
+ coverage: list[CoverageEntry] = Field(
+ default_factory=list,
+ description="One entry per requirement, including uncovered ones.",
+ )
+ defects: list[str] = Field(
+ default_factory=list,
+ description=(
+ "Problems found by inspecting the scaffold against the design — "
+ "missing endpoints, schema mismatches, unhandled cases."
+ ),
+ )
+ untestable: list[str] = Field(
+ default_factory=list,
+ description="Requirements too vague to test, named so they can be fixed.",
+ )
+
+
+class QAEngineerAgent(BaseAgent[QAEngineerOutput]):
+ """Verifies the implementation against the requirements it came from."""
+
+ role = AgentRole.QA_ENGINEER
+ stage = LifecycleStage.TESTING
+ output_model = QAEngineerOutput
+ prompt_name = "qa_engineer"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Plan and specify testing for **{context.project_name}**.\n\n"
+ "Write test cases (TC-nn) in given/when/then form. Every case must "
+ "quote the acceptance criterion it verifies and name the requirement "
+ "IDs it covers — a test that cannot be traced to a requirement is a "
+ "test nobody can justify keeping.\n\n"
+ "Report coverage per requirement, including the ones with no test, "
+ "and say why. Inspect the generated scaffold against the architecture "
+ "and API contract, and record real mismatches in `defects`.\n\n"
+ "If a requirement is too vague to test, put it in `untestable` rather "
+ "than inventing an interpretation."
+ )
+
+ def compose_artifacts(
+ self, output: QAEngineerOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ links = self._links(output)
+ covered = sum(1 for entry in output.coverage if entry.covered)
+ total = len(output.coverage)
+
+ plan = sections(
+ heading(f"Test Plan — {context.project_name}"),
+ heading("Strategy", 2),
+ paragraph(output.strategy),
+ heading("Defects found", 2),
+ bullets(output.defects),
+ heading("Requirements that cannot be tested as written", 2),
+ bullets(output.untestable),
+ )
+
+ cases = sections(
+ heading(f"Test Cases — {context.project_name}"),
+ table(
+ ["ID", "Title", "Kind", "Given", "When", "Then", "Verifies"],
+ [
+ [
+ case.id,
+ case.title,
+ case.kind,
+ case.given,
+ case.when,
+ case.then,
+ case.acceptance_criteria or ", ".join(case.requirement_ids),
+ ]
+ for case in output.test_cases
+ ],
+ ),
+ )
+
+ coverage = sections(
+ heading(f"Coverage Report — {context.project_name}"),
+ paragraph(
+ f"**{covered} of {total} requirements covered**"
+ if total
+ else "_No requirements were available to assess._"
+ ),
+ paragraph(
+ "Coverage is measured against requirements rather than code, so "
+ "an uncovered requirement is visible as a gap rather than hidden "
+ "behind a percentage."
+ ),
+ table(
+ ["Requirement", "Covered", "Test cases", "Note"],
+ [
+ [
+ entry.requirement_id,
+ "yes" if entry.covered else "no",
+ entry.test_case_ids,
+ entry.note,
+ ]
+ for entry in output.coverage
+ ],
+ ),
+ )
+
+ return [
+ ArtifactDraft(
+ type=ArtifactType.TEST_PLAN,
+ title=f"Test Plan — {context.project_name}",
+ body_markdown=plan,
+ content={
+ "strategy": output.strategy,
+ "defects": output.defects,
+ "untestable": output.untestable,
+ },
+ summary=f"{len(output.defects)} defects, {len(output.untestable)} untestable",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.TEST_CASES,
+ title=f"Test Cases — {context.project_name}",
+ body_markdown=cases,
+ content={
+ "test_cases": [case.model_dump(mode="json") for case in output.test_cases]
+ },
+ summary=f"{len(output.test_cases)} test cases",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.COVERAGE_REPORT,
+ title=f"Coverage Report — {context.project_name}",
+ body_markdown=coverage,
+ content={
+ "covered": covered,
+ "total": total,
+ "entries": [entry.model_dump(mode="json") for entry in output.coverage],
+ },
+ summary=f"{covered}/{total} requirements covered",
+ derived_from=links,
+ ),
+ ]
diff --git a/submissions/Victorious/apps/api/app/agents/rendering.py b/submissions/Victorious/apps/api/app/agents/rendering.py
new file mode 100644
index 00000000..ccd3db5b
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/rendering.py
@@ -0,0 +1,77 @@
+"""Markdown rendering for engineering artifacts.
+
+Artifacts are rendered from an agent's structured output rather than written as
+prose by the model. That guarantees the document a human reads and the data a
+downstream agent consumes are the same information, and it keeps formatting
+consistent across every project the platform builds.
+
+These helpers are deliberately plain: tables, headings, and lists. The output is
+read in the workspace and exported into generated repositories, so it must stay
+legible as raw text.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Iterable, Sequence
+
+
+def heading(text: str, level: int = 1) -> str:
+ return f"{'#' * level} {text}"
+
+
+def paragraph(text: str) -> str:
+ return text.strip()
+
+
+def bullets(items: Iterable[str]) -> str:
+ """Render a bullet list, or an explicit note when empty.
+
+ An empty section says so rather than rendering nothing: a heading with no
+ content reads as a formatting bug, while "None identified" is a finding.
+ """
+ rendered = [f"- {item}" for item in items if item]
+ return "\n".join(rendered) if rendered else "_None identified._"
+
+
+def table(headers: Sequence[str], rows: Iterable[Sequence[object]]) -> str:
+ """Render a markdown table, escaping pipes in cell content.
+
+ Cells accept any value, not just strings: agent contracts hold lists of
+ identifiers (``requirement_ids``, ``depends_on``) that read naturally as
+ comma-separated cells, and forcing every call site to join them first would
+ duplicate that formatting across every agent.
+ """
+ materialised = [[_cell(value) for value in row] for row in rows]
+
+ if not materialised:
+ return "_None identified._"
+
+ lines = [
+ "| " + " | ".join(headers) + " |",
+ "| " + " | ".join("---" for _ in headers) + " |",
+ ]
+ lines.extend("| " + " | ".join(row) + " |" for row in materialised)
+ return "\n".join(lines)
+
+
+def code_block(content: str, language: str = "") -> str:
+ """Fence a code block, widening the fence if the content contains one."""
+ fence = "```"
+ while fence in content:
+ fence += "`"
+ return f"{fence}{language}\n{content}\n{fence}"
+
+
+def sections(*parts: str) -> str:
+ """Join non-empty sections with blank lines between them."""
+ return "\n\n".join(part.strip() for part in parts if part and part.strip())
+
+
+def _cell(value: object) -> str:
+ """Flatten a value into one table cell.
+
+ Newlines are replaced rather than escaped: a literal newline inside a cell
+ breaks the table row entirely in every markdown renderer.
+ """
+ text = ", ".join(str(item) for item in value) if isinstance(value, list) else str(value)
+ return text.replace("|", "\\|").replace("\n", " ").strip() or "—"
diff --git a/submissions/Victorious/apps/api/app/agents/software_architect.py b/submissions/Victorious/apps/api/app/agents/software_architect.py
new file mode 100644
index 00000000..ac366803
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/agents/software_architect.py
@@ -0,0 +1,305 @@
+"""Software Architect Agent — architecture and development planning.
+
+`05_AI_Agent_Architecture.md`: system architecture, component design, service
+decomposition, scalability planning, API planning, technology recommendations.
+
+Two stages, two agents, one role. The architect designs the system in
+``ARCHITECTURE`` and sequences the work in ``DEVELOPMENT_PLANNING``. They are
+separate agents because each has a distinct output contract and a distinct
+approval gate between them — `09_MVP_Roadmap.md` requires architecture sign-off
+before work is planned against it.
+"""
+
+from __future__ import annotations
+
+from pydantic import Field
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput, ArtifactDraft
+from app.agents.models import (
+ ApiEndpoint,
+ Component,
+ DataEntity,
+ ImplementationTask,
+ TechnologyChoice,
+)
+from app.agents.rendering import bullets, code_block, heading, paragraph, sections, table
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.memory.context_builder import ProjectContext
+
+
+class ArchitectOutput(AgentOutput):
+ """What the Software Architect produces during design."""
+
+ style: str = Field(
+ description="The architectural style chosen, e.g. 'modular monolith'."
+ )
+ style_rationale: str = Field(
+ description="Why this style suits these requirements and this scale."
+ )
+
+ components: list[Component] = Field(default_factory=list)
+ technology_choices: list[TechnologyChoice] = Field(default_factory=list)
+ api_endpoints: list[ApiEndpoint] = Field(default_factory=list)
+ data_entities: list[DataEntity] = Field(default_factory=list)
+
+ scalability_notes: list[str] = Field(default_factory=list)
+ security_notes: list[str] = Field(
+ default_factory=list,
+ description="Authentication, authorisation, and data-protection decisions.",
+ )
+
+
+class SoftwareArchitectAgent(BaseAgent[ArchitectOutput]):
+ """Turns validated requirements into a system design."""
+
+ role = AgentRole.SOFTWARE_ARCHITECT
+ stage = LifecycleStage.ARCHITECTURE
+ output_model = ArchitectOutput
+ prompt_name = "software_architect"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Design the system architecture for **{context.project_name}**.\n\n"
+ "Every component must trace to the requirements it serves, and every "
+ "technology choice must name what you considered and rejected, with "
+ "the trade-off you accepted. A choice recorded without alternatives "
+ "cannot be reviewed, and a human approves this before implementation.\n\n"
+ "Take the Business Analyst's questioned requirements and gaps "
+ "seriously. If a gap blocks a sound design, raise it in `concerns` "
+ "rather than designing around it silently.\n\n"
+ "Set `requires_approval` when your technology selections commit the "
+ "project to something costly to reverse."
+ )
+
+ def compose_artifacts(
+ self, output: ArchitectOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ links = self._links(output)
+
+ architecture = sections(
+ heading(f"System Architecture — {context.project_name}"),
+ heading("Architectural style", 2),
+ paragraph(f"**{output.style}**"),
+ paragraph(output.style_rationale),
+ heading("Components", 2),
+ table(
+ ["Component", "Responsibility", "Depends on", "Requirements"],
+ [
+ [item.name, item.responsibility, item.depends_on, item.requirement_ids]
+ for item in output.components
+ ],
+ ),
+ heading("Component diagram", 2),
+ code_block(_mermaid(output.components), "mermaid"),
+ heading("Scalability", 2),
+ bullets(output.scalability_notes),
+ heading("Security", 2),
+ bullets(output.security_notes),
+ )
+
+ technology = sections(
+ heading(f"Technology Decisions — {context.project_name}"),
+ paragraph(
+ "Each decision records what was considered and what the choice "
+ "costs. These require human approval before implementation."
+ ),
+ table(
+ ["Layer", "Choice", "Alternatives considered", "Rationale", "Trade-offs"],
+ [
+ [
+ item.layer,
+ item.choice,
+ item.alternatives,
+ item.rationale,
+ item.tradeoffs,
+ ]
+ for item in output.technology_choices
+ ],
+ ),
+ )
+
+ api = sections(
+ heading(f"API Contract — {context.project_name}"),
+ table(
+ ["Method", "Path", "Purpose", "Request", "Response", "Requirements"],
+ [
+ [
+ item.method,
+ item.path,
+ item.purpose,
+ item.request_summary,
+ item.response_summary,
+ item.requirement_ids,
+ ]
+ for item in output.api_endpoints
+ ],
+ ),
+ )
+
+ schema = sections(
+ heading(f"Database Schema — {context.project_name}"),
+ *[
+ sections(
+ heading(entity.name, 2),
+ paragraph(entity.purpose),
+ table(
+ ["Field", "Type", "Nullable", "Description"],
+ [
+ [field.name, field.type, str(field.nullable), field.description]
+ for field in entity.fields
+ ],
+ ),
+ heading("Relationships", 3),
+ bullets(entity.relationships),
+ )
+ for entity in output.data_entities
+ ],
+ )
+
+ return [
+ ArtifactDraft(
+ type=ArtifactType.SYSTEM_ARCHITECTURE,
+ title=f"System Architecture — {context.project_name}",
+ body_markdown=architecture,
+ content={
+ "style": output.style,
+ "components": [c.model_dump(mode="json") for c in output.components],
+ "scalability_notes": output.scalability_notes,
+ "security_notes": output.security_notes,
+ },
+ summary=f"{output.style} with {len(output.components)} components",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.TECHNOLOGY_DECISION,
+ title=f"Technology Decisions — {context.project_name}",
+ body_markdown=technology,
+ content={
+ "choices": [
+ c.model_dump(mode="json") for c in output.technology_choices
+ ]
+ },
+ summary=f"{len(output.technology_choices)} technology decisions",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.API_CONTRACT,
+ title=f"API Contract — {context.project_name}",
+ body_markdown=api,
+ content={
+ "endpoints": [e.model_dump(mode="json") for e in output.api_endpoints]
+ },
+ summary=f"{len(output.api_endpoints)} endpoints",
+ derived_from=links,
+ ),
+ ArtifactDraft(
+ type=ArtifactType.DATABASE_SCHEMA,
+ title=f"Database Schema — {context.project_name}",
+ body_markdown=schema,
+ content={
+ "entities": [e.model_dump(mode="json") for e in output.data_entities]
+ },
+ summary=f"{len(output.data_entities)} entities",
+ derived_from=links,
+ ),
+ ]
+
+
+class ImplementationPlanOutput(AgentOutput):
+ """What the Software Architect produces while planning the build."""
+
+ sequencing_rationale: str = Field(
+ description="Why the work is ordered this way, in terms of risk and dependency."
+ )
+ tasks: list[ImplementationTask] = Field(default_factory=list)
+ milestones: list[str] = Field(
+ default_factory=list, description="Checkpoints where something is demonstrable."
+ )
+
+
+class ImplementationPlannerAgent(BaseAgent[ImplementationPlanOutput]):
+ """Breaks an approved architecture into ordered, dependency-aware work."""
+
+ role = AgentRole.SOFTWARE_ARCHITECT
+ stage = LifecycleStage.DEVELOPMENT_PLANNING
+ output_model = ImplementationPlanOutput
+ prompt_name = "implementation_planner"
+
+ def describe_task(self) -> str:
+ return "Software Architect · implementation planning"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return (
+ f"Sequence the implementation of **{context.project_name}** from the "
+ "approved architecture.\n\n"
+ "Produce tasks (T-nn) that each name the component they belong to, "
+ "the requirements they satisfy, and the tasks they depend on. Order "
+ "the work so that the riskiest and most foundational parts are built "
+ "first — a plan that defers the hard part is not a plan.\n\n"
+ "Every task must be small enough that its completion is unambiguous."
+ )
+
+ def compose_artifacts(
+ self, output: ImplementationPlanOutput, context: ProjectContext
+ ) -> list[ArtifactDraft]:
+ plan = sections(
+ heading(f"Implementation Plan — {context.project_name}"),
+ heading("Sequencing rationale", 2),
+ paragraph(output.sequencing_rationale),
+ heading("Tasks", 2),
+ table(
+ ["ID", "Task", "Component", "Depends on", "Requirements", "Estimate"],
+ [
+ [
+ task.id,
+ task.title,
+ task.component,
+ task.depends_on,
+ task.requirement_ids,
+ task.estimate,
+ ]
+ for task in output.tasks
+ ],
+ ),
+ heading("Milestones", 2),
+ bullets(output.milestones),
+ )
+
+ return [
+ ArtifactDraft(
+ type=ArtifactType.IMPLEMENTATION_PLAN,
+ title=f"Implementation Plan — {context.project_name}",
+ body_markdown=plan,
+ content={
+ "tasks": [task.model_dump(mode="json") for task in output.tasks],
+ "milestones": output.milestones,
+ },
+ summary=f"{len(output.tasks)} tasks across {len(output.milestones)} milestones",
+ derived_from=self._links(output),
+ )
+ ]
+
+
+def _mermaid(components: list[Component]) -> str:
+ """Render a component dependency diagram.
+
+ Mermaid because the workspace renders it natively and it stays readable as
+ raw text inside a generated repository.
+ """
+ if not components:
+ return "graph TD\n empty[No components defined]"
+
+ lines = ["graph TD"]
+ aliases = {component.name: f"c{index}" for index, component in enumerate(components)}
+
+ for component in components:
+ lines.append(f' {aliases[component.name]}["{component.name}"]')
+
+ for component in components:
+ for dependency in component.depends_on:
+ if dependency in aliases:
+ lines.append(f" {aliases[component.name]} --> {aliases[dependency]}")
+
+ return "\n".join(lines)
diff --git a/submissions/Victorious/apps/api/app/api/__init__.py b/submissions/Victorious/apps/api/app/api/__init__.py
new file mode 100644
index 00000000..b93c2a27
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/__init__.py
@@ -0,0 +1,5 @@
+"""HTTP transport layer.
+
+Translates requests into calls on inner layers and domain results back into
+responses. Contains no engineering logic of its own.
+"""
diff --git a/submissions/Victorious/apps/api/app/api/deps.py b/submissions/Victorious/apps/api/app/api/deps.py
new file mode 100644
index 00000000..00092d22
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/deps.py
@@ -0,0 +1,72 @@
+"""FastAPI dependency providers.
+
+Bridges the application-scoped container onto FastAPI's request-scoped injection
+so routers declare what they need as typed parameters and never reach for a
+global.
+"""
+
+from __future__ import annotations
+
+from typing import Annotated
+
+from fastapi import Depends, Request
+
+from app.core.config import Settings
+from app.core.container import Container
+from app.core.health import HealthRegistry
+from app.events.bus import EventBus
+from app.memory.repository import SharedMemory
+from app.orchestration.runner import OrchestrationRunner
+
+
+def get_container(request: Request) -> Container:
+ """Return the container attached to the application at startup."""
+ container: Container = request.app.state.container
+ return container
+
+
+def get_settings_dep(container: Annotated[Container, Depends(get_container)]) -> Settings:
+ """Resolve application settings."""
+ return container.resolve(Settings)
+
+
+def get_health_registry(
+ container: Annotated[Container, Depends(get_container)],
+) -> HealthRegistry:
+ """Resolve the health check registry."""
+ return container.resolve(HealthRegistry)
+
+
+def get_memory(container: Annotated[Container, Depends(get_container)]) -> SharedMemory:
+ """Resolve the shared organizational memory.
+
+ Resolved by protocol, not by concrete class, so the SQL implementation could
+ be swapped without touching a single router (ADR-0003).
+ """
+ return container.resolve(SharedMemory) # type: ignore[type-abstract]
+
+
+def get_runner(
+ container: Annotated[Container, Depends(get_container)],
+) -> OrchestrationRunner:
+ """Resolve the orchestration runner."""
+ return container.resolve(OrchestrationRunner)
+
+
+def get_event_bus(container: Annotated[Container, Depends(get_container)]) -> EventBus:
+ """Resolve the event bus.
+
+ A process-wide singleton: the publisher is whichever request is advancing the
+ workflow, and subscribers are the open streams. They only meet if they share
+ one instance.
+ """
+ return container.resolve(EventBus)
+
+
+# Named aliases keep router signatures readable as the dependency set grows.
+ContainerDep = Annotated[Container, Depends(get_container)]
+SettingsDep = Annotated[Settings, Depends(get_settings_dep)]
+HealthRegistryDep = Annotated[HealthRegistry, Depends(get_health_registry)]
+MemoryDep = Annotated[SharedMemory, Depends(get_memory)]
+RunnerDep = Annotated[OrchestrationRunner, Depends(get_runner)]
+EventBusDep = Annotated[EventBus, Depends(get_event_bus)]
diff --git a/submissions/Victorious/apps/api/app/api/routers/__init__.py b/submissions/Victorious/apps/api/app/api/routers/__init__.py
new file mode 100644
index 00000000..b370ea7b
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/routers/__init__.py
@@ -0,0 +1 @@
+"""API routers, one module per resource."""
diff --git a/submissions/Victorious/apps/api/app/api/routers/health.py b/submissions/Victorious/apps/api/app/api/routers/health.py
new file mode 100644
index 00000000..8fc47d7d
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/routers/health.py
@@ -0,0 +1,60 @@
+"""Health and readiness endpoints.
+
+Mounted outside the versioned API prefix: orchestrators and load balancers should
+not have to track the API version to probe the service.
+"""
+
+from __future__ import annotations
+
+from fastapi import APIRouter, Response, status
+
+from app.api.deps import HealthRegistryDep, SettingsDep
+from app.core.health import HealthReport, HealthStatus
+
+router = APIRouter(tags=["health"])
+
+
+@router.get(
+ "/health",
+ summary="Liveness probe",
+ response_model=dict[str, str],
+)
+async def health(settings: SettingsDep) -> dict[str, str]:
+ """Report that the process is alive.
+
+ Deliberately checks nothing external. A liveness probe that fails when a
+ dependency is down causes restart loops that make an outage worse.
+ """
+ return {
+ "status": HealthStatus.HEALTHY.value,
+ "service": settings.app_name,
+ "version": settings.version,
+ "environment": settings.environment.value,
+ }
+
+
+@router.get(
+ "/health/ready",
+ summary="Readiness probe",
+ response_model=HealthReport,
+ responses={503: {"description": "One or more critical components are unavailable."}},
+)
+async def readiness(
+ registry: HealthRegistryDep,
+ settings: SettingsDep,
+ response: Response,
+) -> HealthReport:
+ """Report whether every critical dependency is usable.
+
+ Returns 503 when unhealthy so orchestrators drain traffic; a degraded system
+ still returns 200, because partial capability beats no capability.
+ """
+ report = await registry.evaluate(
+ version=settings.version,
+ environment=settings.environment.value,
+ )
+
+ if not report.is_ready:
+ response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
+
+ return report
diff --git a/submissions/Victorious/apps/api/app/api/routers/projects.py b/submissions/Victorious/apps/api/app/api/routers/projects.py
new file mode 100644
index 00000000..a13cfd65
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/routers/projects.py
@@ -0,0 +1,362 @@
+"""Project lifecycle endpoints.
+
+The whole workspace is served from here: creating a project, advancing it,
+reading its artifacts, agents, approvals, timeline, and traceability graph.
+"""
+
+from __future__ import annotations
+
+from typing import Annotated
+
+from fastapi import APIRouter, Body, Query, status
+
+from app.api import views
+from app.api.deps import MemoryDep, RunnerDep
+from app.api.schemas import (
+ AdvanceResponse,
+ AgentCard,
+ ApprovalDecisionRequest,
+ ApprovalView,
+ ArtifactDetail,
+ ArtifactSummary,
+ CreateProjectRequest,
+ EventView,
+ ImpactPreview,
+ ProjectDetail,
+ ProjectReviewSummary,
+ ProjectSummary,
+ ReviseArtifactRequest,
+ TraceGraph,
+)
+from app.core.logging import get_logger
+from app.domain.approvals import ApprovalStatus
+from app.domain.artifacts import ArtifactType, ArtifactVersion
+from app.domain.errors import ValidationError
+from app.domain.events import EventType, ProjectEvent
+from app.domain.lifecycle import LifecycleStage
+from app.domain.projects import Project
+
+logger = get_logger(__name__)
+
+router = APIRouter(prefix="/projects", tags=["projects"])
+
+
+@router.post(
+ "",
+ response_model=ProjectSummary,
+ status_code=status.HTTP_201_CREATED,
+ summary="Create a project",
+)
+async def create_project(
+ request: CreateProjectRequest, memory: MemoryDep
+) -> ProjectSummary:
+ """Start a project from a name and a description.
+
+ Nothing else is asked for. `07_System_Architecture.md` requires the user to
+ enter the workspace immediately, with the organization discovering
+ requirements from there rather than through an upfront interview.
+ """
+ project = await memory.projects.create(
+ Project(name=request.name, description=request.description)
+ )
+
+ await memory.events.append(
+ ProjectEvent(
+ project_id=project.id,
+ type=EventType.PROJECT_CREATED,
+ summary=f"Project created: {project.name}",
+ payload={"name": project.name},
+ )
+ )
+
+ logger.info("Project created", extra={"project_id": project.id})
+ return await views.project_summary(memory, project.id)
+
+
+@router.get("", response_model=list[ProjectSummary], summary="List projects")
+async def list_projects(
+ memory: MemoryDep, limit: Annotated[int, Query(ge=1, le=100)] = 50
+) -> list[ProjectSummary]:
+ return await views.list_projects(memory, limit=limit)
+
+
+@router.get("/{project_id}", response_model=ProjectDetail, summary="Project detail")
+async def get_project(project_id: str, memory: MemoryDep) -> ProjectDetail:
+ return await views.project_detail(memory, project_id)
+
+
+@router.post(
+ "/{project_id}/advance",
+ response_model=AdvanceResponse,
+ summary="Advance the engineering workflow",
+)
+async def advance_project(project_id: str, runner: RunnerDep) -> AdvanceResponse:
+ """Let the organization work until it needs a human.
+
+ Returns when an approval gate is reached, a blocking conflict is found, or
+ the lifecycle completes. Calling again after a decision resumes — state lives
+ in shared memory, not in the graph (ADR-0009).
+ """
+ outcome = await runner.advance(project_id)
+
+ return AdvanceResponse(
+ project_id=outcome.project_id,
+ executed_stages=outcome.executed_stages,
+ halt_action=outcome.halt_action.value if outcome.halt_action else None,
+ halt_reason=outcome.halt_reason,
+ pending_approval_id=outcome.pending_approval_id,
+ conflicts=outcome.conflicts,
+ error=outcome.error,
+ )
+
+
+@router.get(
+ "/{project_id}/artifacts",
+ response_model=list[ArtifactSummary],
+ summary="List engineering artifacts",
+)
+async def list_artifacts(
+ project_id: str,
+ memory: MemoryDep,
+ stage: LifecycleStage | None = None,
+ # Exposed as `?type=` because that reads naturally in a URL, while the
+ # parameter avoids shadowing the builtin.
+ artifact_type: Annotated[ArtifactType | None, Query(alias="type")] = None,
+) -> list[ArtifactSummary]:
+ return await views.list_artifacts(
+ memory, project_id, stage=stage, artifact_type=artifact_type
+ )
+
+
+@router.get(
+ "/{project_id}/agents",
+ response_model=list[AgentCard],
+ summary="The AI engineering organization",
+)
+async def get_organization(project_id: str, memory: MemoryDep) -> list[AgentCard]:
+ """Every specialist and its current state, including those not yet running."""
+ return await views.organization(memory, project_id)
+
+
+@router.get(
+ "/{project_id}/approvals",
+ response_model=list[ApprovalView],
+ summary="Approval requests",
+)
+async def list_approvals(
+ project_id: str, memory: MemoryDep, pending: bool = False
+) -> list[ApprovalView]:
+ return await views.list_approvals(memory, project_id, pending_only=pending)
+
+
+@router.get(
+ "/{project_id}/events",
+ response_model=list[EventView],
+ summary="Engineering timeline",
+)
+async def list_events(
+ project_id: str,
+ memory: MemoryDep,
+ limit: Annotated[int, Query(ge=1, le=500)] = 200,
+ after: str | None = None,
+) -> list[EventView]:
+ """Activity oldest first. ``after`` resumes from a known event id."""
+ return await views.list_events(memory, project_id, limit=limit, after_id=after)
+
+
+@router.get(
+ "/{project_id}/reviews",
+ response_model=ProjectReviewSummary,
+ summary="Helix Review — engineering quality of every artifact",
+)
+async def get_reviews(project_id: str, memory: MemoryDep) -> ProjectReviewSummary:
+ """Overall score, per-specialist scores, recommendations, and full history.
+
+ Reviews come from the organization's own review layer. Helix — Mutagent's ADL
+ conductor — specifies and evaluates that reviewer at development time, and is
+ deliberately absent from this request path: `07_System_Architecture.md` keeps
+ Mutagent outside the runtime execution path.
+ """
+ summary: ProjectReviewSummary = await views.project_reviews(memory, project_id)
+ return summary
+
+
+@router.get(
+ "/{project_id}/traceability",
+ response_model=TraceGraph,
+ summary="Traceability graph",
+)
+async def get_traceability(project_id: str, memory: MemoryDep) -> TraceGraph:
+ """Every artifact and the dependencies between them, with staleness resolved."""
+ return await views.trace_graph(memory, project_id)
+
+
+@router.get(
+ "/{project_id}/artifacts/{artifact_id}",
+ response_model=ArtifactDetail,
+ summary="Artifact detail",
+)
+async def get_artifact(
+ project_id: str,
+ artifact_id: str,
+ memory: MemoryDep,
+ version: Annotated[int | None, Query(ge=1)] = None,
+) -> ArtifactDetail:
+ """One artifact with its content and full version history.
+
+ Omitting ``version`` returns the latest. Any earlier version remains readable
+ exactly as the agent that consumed it saw it (ADR-0007).
+ """
+ return await views.artifact_detail(memory, artifact_id, version=version)
+
+
+@router.get(
+ "/{project_id}/artifacts/{artifact_id}/impact",
+ response_model=ImpactPreview,
+ summary="What changing this artifact would affect",
+)
+async def get_impact(
+ project_id: str, artifact_id: str, memory: MemoryDep
+) -> ImpactPreview:
+ """Compute the blast radius of a change without making one.
+
+ The question `04_Existing_Solutions.md` says no tool answers, asked *before*
+ the change rather than reported after it.
+ """
+ return await views.impact_preview(memory, project_id, artifact_id)
+
+
+@router.post(
+ "/{project_id}/artifacts/{artifact_id}/revise",
+ response_model=ArtifactDetail,
+ summary="Revise an artifact",
+)
+async def revise_artifact(
+ project_id: str,
+ artifact_id: str,
+ memory: MemoryDep,
+ revision: Annotated[ReviseArtifactRequest, Body()],
+) -> ArtifactDetail:
+ """Append a human-authored revision to an artifact.
+
+ The change a user makes when a requirement turns out to be wrong. It appends
+ a version rather than editing in place, so the version the agents downstream
+ consumed stays readable, and every traceability edge pointing at this
+ artifact now cites an older version than it currently has — which is exactly
+ how those downstream artifacts become stale (ADR-0007).
+
+ Nothing is regenerated here. The impact is computed and returned so the user
+ can see the blast radius before deciding what to do about it.
+ """
+ artifact = await memory.artifacts.get(artifact_id)
+
+ if artifact.project_id != project_id:
+ raise ValidationError(
+ "Artifact does not belong to this project",
+ details={"artifact_id": artifact_id, "project_id": project_id},
+ )
+
+ await memory.artifacts.append_version(
+ artifact_id,
+ ArtifactVersion(
+ artifact_id=artifact_id,
+ version=1, # Assigned by the repository.
+ body_markdown=revision.body_markdown,
+ content=artifact_content_or_empty(revision),
+ summary=revision.summary or "Revised by a human",
+ ),
+ )
+
+ impact = await memory.traces.analyse_impact(project_id, artifact_id)
+
+ await memory.events.append(
+ ProjectEvent(
+ project_id=project_id,
+ type=EventType.ARTIFACT_REVISED,
+ stage=artifact.stage,
+ summary=f"{artifact.title} was revised — {len(impact.impacted)} artifacts affected",
+ payload={
+ "artifact_id": artifact_id,
+ "impacted_artifact_ids": impact.artifact_ids,
+ },
+ )
+ )
+
+ logger.info(
+ "Artifact revised",
+ extra={
+ "project_id": project_id,
+ "artifact_id": artifact_id,
+ "impacted": len(impact.impacted),
+ },
+ )
+ return await views.artifact_detail(memory, artifact_id)
+
+
+def artifact_content_or_empty(revision: ReviseArtifactRequest) -> dict[str, object]:
+ """Structured content for a human revision.
+
+ A person edits prose, not the structured fields an agent emits. Carrying the
+ previous structure forward would leave it describing text that no longer
+ exists, so it is dropped and the markdown becomes the truth for this version.
+ """
+ return dict(revision.content or {})
+
+
+approvals_router = APIRouter(prefix="/approvals", tags=["approvals"])
+
+
+@approvals_router.post(
+ "/{approval_id}/decision",
+ response_model=ApprovalView,
+ summary="Decide an approval request",
+)
+async def decide_approval(
+ approval_id: str,
+ memory: MemoryDep,
+ runner: RunnerDep,
+ decision: Annotated[ApprovalDecisionRequest, Body()],
+) -> ApprovalView:
+ """Approve, reject, or request changes.
+
+ Rejecting requires feedback: it is fed back into the agent's context on
+ re-run, so a rejection teaches rather than repeats. Rejecting without saying
+ why would leave the organization to guess.
+
+ The consequences of a decision — approving the reviewed artifacts, or
+ reopening the stage that produced them — belong to the Executive AI, which
+ coordinates the organization. This endpoint only validates and delegates.
+ """
+ if decision.decision is ApprovalStatus.PENDING:
+ raise ValidationError(
+ "A decision cannot be 'pending'",
+ details={"allowed": ["approved", "rejected", "changes_requested"]},
+ )
+
+ if not decision.decision.unblocks_progress and not (decision.feedback or "").strip():
+ raise ValidationError(
+ "Feedback is required when not approving",
+ details={"decision": decision.decision.value},
+ )
+
+ request = await runner.executive.record_decision(
+ approval_id, decision.decision, decision.feedback
+ )
+
+ views_list = await views.list_approvals(memory, request.project_id)
+ return next(view for view in views_list if view.id == approval_id)
+
+
+@approvals_router.get(
+ "", response_model=list[ApprovalView], summary="Pending approvals across projects"
+)
+async def list_pending(memory: MemoryDep) -> list[ApprovalView]:
+ """Everything waiting on a human, for the dashboard."""
+ pending = await memory.approvals.list_pending()
+
+ collected: list[ApprovalView] = []
+ for project_id in dict.fromkeys(request.project_id for request in pending):
+ collected.extend(
+ await views.list_approvals(memory, project_id, pending_only=True)
+ )
+ return collected
diff --git a/submissions/Victorious/apps/api/app/api/routers/stream.py b/submissions/Victorious/apps/api/app/api/routers/stream.py
new file mode 100644
index 00000000..a7d98f27
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/routers/stream.py
@@ -0,0 +1,127 @@
+"""Live engineering activity stream.
+
+`07_System_Architecture.md` requires users to "observe the complete AI
+Engineering Organization operating in real time", and `10_UI_UX_Plan.md`
+forbids hiding agent execution behind loading indicators. This endpoint is how
+the workspace sees work as it happens rather than after it finishes.
+
+The stream carries *signals*, not state. A client reacts to an event by re-reading
+the REST endpoint it cares about, so there is exactly one projection of agent and
+artifact state — the API's — rather than a second one assembled in the browser
+that can drift from it.
+"""
+
+from __future__ import annotations
+
+import asyncio
+from collections.abc import AsyncIterator
+
+from fastapi import APIRouter, Header, Request
+from fastapi.responses import StreamingResponse
+
+from app.api.deps import EventBusDep, MemoryDep
+from app.core.logging import get_logger
+from app.events.bus import EventBus
+from app.events.sse import (
+ HEARTBEAT_SECONDS,
+ format_event,
+ format_heartbeat,
+ format_open,
+ format_retry,
+)
+from app.memory.repository import SharedMemory
+
+logger = get_logger(__name__)
+
+router = APIRouter(prefix="/projects", tags=["stream"])
+
+#: Events replayed at most on reconnection. A browser that has been closed for an
+#: hour wants recent history, not the entire project.
+REPLAY_LIMIT = 200
+
+
+async def _stream(
+ request: Request,
+ bus: EventBus,
+ memory: SharedMemory,
+ project_id: str,
+ last_event_id: str | None,
+ *,
+ heartbeat_seconds: float = HEARTBEAT_SECONDS,
+) -> AsyncIterator[str]:
+ """Yield SSE frames until the client disconnects.
+
+ Subscription happens *before* the replay read, deliberately. Reading first
+ and subscribing afterwards would drop anything published in between — a
+ window that lands precisely when the organization is busiest, which is when
+ the stream matters most. Subscribing first can instead duplicate an event
+ that appears in both, so replayed identifiers are remembered and skipped.
+
+ ``heartbeat_seconds`` is injectable so tests exercise the idle path without
+ waiting the production interval for every case.
+ """
+ async with bus.subscribe(project_id) as queue:
+ yield format_retry()
+ yield format_open()
+
+ replayed: set[str] = set()
+ for event in await memory.events.list_for_project(
+ project_id, limit=REPLAY_LIMIT, after_id=last_event_id
+ ):
+ replayed.add(event.id)
+ yield format_event(event)
+
+ while True:
+ if await request.is_disconnected():
+ break
+
+ try:
+ event = await asyncio.wait_for(queue.get(), timeout=heartbeat_seconds)
+ except TimeoutError:
+ yield format_heartbeat()
+ continue
+
+ if event.id in replayed:
+ continue
+
+ # The replay set only guards the handover; once a live event arrives
+ # the window has closed and holding the set would leak.
+ replayed.clear()
+ yield format_event(event)
+
+ logger.debug("Event stream closed", extra={"project_id": project_id})
+
+
+@router.get(
+ "/{project_id}/events/stream",
+ summary="Live engineering activity (SSE)",
+ response_class=StreamingResponse,
+)
+async def stream_events(
+ project_id: str,
+ request: Request,
+ bus: EventBusDep,
+ memory: MemoryDep,
+ last_event_id: str | None = Header(default=None, alias="Last-Event-ID"),
+) -> StreamingResponse:
+ """Stream the project's engineering activity as it happens.
+
+ Reconnecting browsers send ``Last-Event-ID`` automatically; everything missed
+ while disconnected is replayed before the live feed resumes.
+ """
+ # Existence is checked before the stream opens: a 404 inside a streaming
+ # response would arrive as a 200 with an error frame, which no EventSource
+ # client would treat as a failure.
+ await memory.projects.get(project_id)
+
+ return StreamingResponse(
+ _stream(request, bus, memory, project_id, last_event_id),
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache, no-transform",
+ "Connection": "keep-alive",
+ # Nginx buffers proxied responses by default, which would hold every
+ # frame until the stream ended — defeating the point entirely.
+ "X-Accel-Buffering": "no",
+ },
+ )
diff --git a/submissions/Victorious/apps/api/app/api/schemas.py b/submissions/Victorious/apps/api/app/api/schemas.py
new file mode 100644
index 00000000..56364a11
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/schemas.py
@@ -0,0 +1,571 @@
+"""Request and response models for the HTTP API.
+
+Separate from the domain models on purpose. Domain models express engineering
+meaning and change when the engineering model changes; these express a wire
+contract and change when clients need them to. Collapsing the two would make
+every domain refactor a breaking API change.
+
+Response models are also where field selection happens: an artifact list must not
+carry every version body, or the Knowledge Base would transfer megabytes to
+render a table of contents.
+"""
+
+from __future__ import annotations
+
+from datetime import datetime
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from app.domain.agents import AgentRun
+from app.domain.approvals import ApprovalKind, ApprovalRequest, ApprovalStatus
+from app.domain.artifacts import Artifact, ArtifactStatus, ArtifactType, ArtifactWithVersion
+from app.domain.events import EventType, ProjectEvent
+from app.domain.lifecycle import ROLE_TITLES, AgentRole, LifecycleStage, StageStatus
+from app.domain.projects import Project
+from app.domain.reviews import ArtifactReview, ReviewVerdict
+
+# --- Projects -----------------------------------------------------------------
+
+
+class CreateProjectRequest(BaseModel):
+ """Everything needed to start a project.
+
+ Two fields, deliberately. `07_System_Architecture.md`: "Every project begins
+ with minimal onboarding by asking only for a project name and a brief
+ description, allowing users to enter the workspace immediately without
+ lengthy setup."
+ """
+
+ name: str = Field(min_length=1, max_length=200)
+ description: str = Field(min_length=1, max_length=4000)
+
+
+class StageSummary(BaseModel):
+ """One stage's progress, for the Engineering Timeline."""
+
+ stage: LifecycleStage
+ status: StageStatus
+ owner_role: AgentRole | None = None
+ owner_title: str | None = None
+ started_at: datetime | None = None
+ completed_at: datetime | None = None
+ artifact_count: int = 0
+
+
+class ProjectSummary(BaseModel):
+ """A project as it appears in a list."""
+
+ model_config = ConfigDict(from_attributes=True)
+
+ id: str
+ name: str
+ description: str
+ current_stage: LifecycleStage
+ completed_stages: int
+ total_stages: int
+ artifact_count: int = 0
+ pending_approvals: int = 0
+ updated_at: datetime
+
+ @property
+ def progress(self) -> float:
+ return self.completed_stages / self.total_stages if self.total_stages else 0.0
+
+ @classmethod
+ def build(
+ cls,
+ project: Project,
+ *,
+ artifact_count: int = 0,
+ pending_approvals: int = 0,
+ total_stages: int = 8,
+ ) -> ProjectSummary:
+ return cls(
+ id=project.id,
+ name=project.name,
+ description=project.description,
+ current_stage=project.current_stage,
+ completed_stages=len(project.completed_stages),
+ total_stages=total_stages,
+ artifact_count=artifact_count,
+ pending_approvals=pending_approvals,
+ updated_at=project.updated_at,
+ )
+
+
+class ProjectDetail(ProjectSummary):
+ """A project with its full stage timeline."""
+
+ stages: list[StageSummary] = Field(default_factory=list)
+
+
+# --- Artifacts ----------------------------------------------------------------
+
+
+class ArtifactSummary(BaseModel):
+ """An artifact without its body, for lists and the Knowledge Base."""
+
+ id: str
+ project_id: str
+ type: ArtifactType
+ title: str
+ stage: LifecycleStage
+ owner_role: AgentRole
+ owner_title: str
+ status: ArtifactStatus
+ current_version: int
+ is_stale: bool = Field(
+ default=False,
+ description=(
+ "Computed from the traceability graph, never stored — an artifact is "
+ "stale when an inbound edge cites an older version than its upstream "
+ "currently has."
+ ),
+ )
+ updated_at: datetime
+
+ @classmethod
+ def build(cls, artifact: Artifact, *, is_stale: bool = False) -> ArtifactSummary:
+ return cls(
+ id=artifact.id,
+ project_id=artifact.project_id,
+ type=artifact.type,
+ title=artifact.title,
+ stage=artifact.stage,
+ owner_role=artifact.owner_role,
+ owner_title=ROLE_TITLES[artifact.owner_role],
+ status=artifact.status,
+ current_version=artifact.current_version,
+ is_stale=is_stale,
+ updated_at=artifact.updated_at,
+ )
+
+
+class VersionSummary(BaseModel):
+ """One entry of an artifact's version history."""
+
+ version: int
+ summary: str
+ confidence: float | None
+ produced_by_run_id: str | None
+ created_at: datetime
+
+
+class ArtifactDetail(ArtifactSummary):
+ """An artifact with one version's content."""
+
+ version: int
+ body_markdown: str
+ content: dict[str, Any] = Field(default_factory=dict)
+ version_summary: str = ""
+ confidence: float | None = None
+ produced_by_run_id: str | None = None
+ is_latest: bool = True
+ versions: list[VersionSummary] = Field(default_factory=list)
+ # Forward reference: ReviewView is declared with the other review models
+ # further down, and the module rebuilds this class once it exists.
+ review: ReviewView | None = Field(
+ default=None, description="The engineering review of this version, if any."
+ )
+
+ @classmethod
+ def from_resolved(
+ cls,
+ resolved: ArtifactWithVersion,
+ *,
+ is_stale: bool = False,
+ versions: list[VersionSummary] | None = None,
+ review: ReviewView | None = None,
+ ) -> ArtifactDetail:
+ """Build from an artifact resolved together with one of its versions.
+
+ Named differently from ``ArtifactSummary.build`` deliberately: it takes a
+ different input type, so overriding would be a Liskov violation dressed
+ up as reuse.
+ """
+ base = ArtifactSummary.build(resolved.artifact, is_stale=is_stale)
+ return cls(
+ **base.model_dump(),
+ version=resolved.version.version,
+ body_markdown=resolved.version.body_markdown,
+ content=resolved.version.content,
+ version_summary=resolved.version.summary,
+ confidence=resolved.version.confidence,
+ produced_by_run_id=resolved.version.produced_by_run_id,
+ is_latest=resolved.is_latest,
+ versions=versions or [],
+ review=review,
+ )
+
+
+# --- Agents -------------------------------------------------------------------
+
+
+class AgentCard(BaseModel):
+ """One agent's state, as the Organization view renders it.
+
+ `10_UI_UX_Plan.md` requires each agent to show current status, assigned
+ responsibilities, current task, confidence level, dependencies, generated
+ outputs, and recent decisions. Keyed by stage rather than role because a role
+ can own two stages (ADR-0010).
+ """
+
+ stage: LifecycleStage
+ role: AgentRole
+ title: str
+ status: str
+ task: str = ""
+ reasoning_summary: str = ""
+ confidence: float | None = None
+ input_artifact_ids: list[str] = Field(default_factory=list)
+ output_artifact_ids: list[str] = Field(default_factory=list)
+ blocked_on: list[str] = Field(default_factory=list)
+ provider: str | None = None
+ model: str | None = None
+ total_tokens: int = 0
+ duration_seconds: float | None = None
+ run_id: str | None = None
+ started_at: datetime | None = None
+
+ @classmethod
+ def idle(cls, stage: LifecycleStage, role: AgentRole) -> AgentCard:
+ """A specialist that has not yet been asked to do anything."""
+ return cls(stage=stage, role=role, title=ROLE_TITLES[role], status="idle")
+
+ @classmethod
+ def from_run(cls, run: AgentRun) -> AgentCard:
+ return cls(
+ stage=run.stage,
+ role=run.role,
+ title=ROLE_TITLES[run.role],
+ status=run.status.value,
+ task=run.task,
+ reasoning_summary=run.reasoning_summary,
+ confidence=run.confidence,
+ input_artifact_ids=run.input_artifact_ids,
+ output_artifact_ids=run.output_artifact_ids,
+ blocked_on=run.blocked_on,
+ provider=run.provider,
+ model=run.model,
+ total_tokens=run.token_usage.total,
+ duration_seconds=run.duration_seconds,
+ run_id=run.id,
+ started_at=run.started_at,
+ )
+
+
+# --- Approvals ----------------------------------------------------------------
+
+
+class ImpactedArtifactView(BaseModel):
+ """One artifact inside a change's blast radius, resolved for display."""
+
+ artifact_id: str
+ title: str
+ type: ArtifactType | None = None
+ depth: int
+ via_kind: str
+
+
+class ApprovalView(BaseModel):
+ """An approval request with the five fields a reviewer needs.
+
+ `10_UI_UX_Plan.md`: what changed, why it changed, which agents were involved,
+ the downstream impact, and the available actions.
+ """
+
+ id: str
+ project_id: str
+ project_name: str = ""
+ kind: ApprovalKind
+ stage: LifecycleStage
+ title: str
+ what_changed: str
+ why: str
+ requested_by: AgentRole
+ agents_involved: list[AgentRole] = Field(default_factory=list)
+ agent_titles: list[str] = Field(default_factory=list)
+ artifacts: list[ArtifactSummary] = Field(default_factory=list)
+ impacted: list[ImpactedArtifactView] = Field(default_factory=list)
+ status: ApprovalStatus
+ feedback: str | None = None
+ created_at: datetime
+ decided_at: datetime | None = None
+
+ @classmethod
+ def build(
+ cls,
+ request: ApprovalRequest,
+ *,
+ project_name: str = "",
+ artifacts: list[ArtifactSummary] | None = None,
+ impacted: list[ImpactedArtifactView] | None = None,
+ ) -> ApprovalView:
+ return cls(
+ id=request.id,
+ project_id=request.project_id,
+ project_name=project_name,
+ kind=request.kind,
+ stage=request.stage,
+ title=request.title,
+ what_changed=request.what_changed,
+ why=request.why,
+ requested_by=request.requested_by,
+ agents_involved=request.agents_involved,
+ agent_titles=[ROLE_TITLES[role] for role in request.agents_involved],
+ artifacts=artifacts or [],
+ impacted=impacted or [],
+ status=request.status,
+ feedback=request.feedback,
+ created_at=request.created_at,
+ decided_at=request.decided_at,
+ )
+
+
+class ReviseArtifactRequest(BaseModel):
+ """A human's revision of an engineering artifact.
+
+ Appends a version; it never edits one. The version downstream agents consumed
+ must stay readable, and the new version is what makes their work stale.
+ """
+
+ body_markdown: str = Field(min_length=1)
+ summary: str = Field(
+ default="",
+ max_length=300,
+ description="What changed, shown in version history and the impact preview.",
+ )
+ content: dict[str, Any] | None = Field(
+ default=None,
+ description=(
+ "Structured content, if the caller has it. Omitted for a prose edit, "
+ "in which case the markdown is the truth for this version."
+ ),
+ )
+
+
+class ApprovalDecisionRequest(BaseModel):
+ """A human's decision on an approval gate."""
+
+ decision: ApprovalStatus = Field(
+ description="approved, rejected, or changes_requested."
+ )
+ feedback: str | None = Field(
+ default=None,
+ max_length=4000,
+ description=(
+ "Required when not approving. Fed back into the agent's context on "
+ "re-run, so a rejection teaches rather than repeats."
+ ),
+ )
+
+
+# --- Events and orchestration -------------------------------------------------
+
+
+class EventView(BaseModel):
+ """One entry of the engineering timeline."""
+
+ id: str
+ type: EventType
+ stage: LifecycleStage | None = None
+ role: AgentRole | None = None
+ role_title: str | None = None
+ summary: str
+ payload: dict[str, Any] = Field(default_factory=dict)
+ created_at: datetime
+
+ @classmethod
+ def build(cls, event: ProjectEvent) -> EventView:
+ return cls(
+ id=event.id,
+ type=event.type,
+ stage=event.stage,
+ role=event.role,
+ role_title=ROLE_TITLES[event.role] if event.role else None,
+ summary=event.summary,
+ payload=event.payload,
+ created_at=event.created_at,
+ )
+
+
+class AdvanceResponse(BaseModel):
+ """The outcome of asking the organization to make progress."""
+
+ project_id: str
+ executed_stages: list[LifecycleStage] = Field(default_factory=list)
+ halt_action: str | None = None
+ halt_reason: str = ""
+ pending_approval_id: str | None = None
+ conflicts: list[dict[str, Any]] = Field(default_factory=list)
+ error: str | None = None
+
+
+# --- Traceability -------------------------------------------------------------
+
+
+class ImpactPreview(BaseModel):
+ """What changing an artifact would affect, computed before it changes.
+
+ `10_UI_UX_Plan.md` requires a reviewer to understand downstream impact before
+ acting. Showing it afterwards would make the platform a report of damage
+ rather than a tool for deciding.
+ """
+
+ artifact_id: str
+ artifact_title: str
+ impacted: list[ImpactedArtifactView] = Field(default_factory=list)
+ stages_affected: list[LifecycleStage] = Field(
+ default_factory=list,
+ description="Stages that would rerun if the change were re-synchronised.",
+ )
+
+ @property
+ def direct_count(self) -> int:
+ return sum(1 for item in self.impacted if item.depth == 1)
+
+
+class TraceNode(BaseModel):
+ """One artifact in the traceability graph."""
+
+ id: str
+ title: str
+ type: ArtifactType
+ stage: LifecycleStage
+ role: AgentRole
+ version: int
+ is_stale: bool = False
+
+
+class TraceEdgeView(BaseModel):
+ """One dependency in the traceability graph."""
+
+ id: str
+ upstream_artifact_id: str
+ downstream_artifact_id: str
+ kind: str
+ upstream_version: int
+ current_upstream_version: int
+ is_stale: bool = False
+ rationale: str = ""
+
+
+class TraceGraph(BaseModel):
+ """The full traceability graph for a project."""
+
+ project_id: str
+ nodes: list[TraceNode] = Field(default_factory=list)
+ edges: list[TraceEdgeView] = Field(default_factory=list)
+ stale_artifact_ids: list[str] = Field(default_factory=list)
+
+
+# --- Engineering review -------------------------------------------------------
+
+
+class ReviewFindingView(BaseModel):
+ """One review observation, with whether it was measured or judged."""
+
+ text: str
+ source: str = Field(description="'check' for a deterministic rule, 'reasoning' otherwise.")
+
+
+class ReviewView(BaseModel):
+ """A scored review of one artifact version."""
+
+ id: str
+ artifact_id: str
+ artifact_title: str = ""
+ artifact_type: ArtifactType | None = None
+ artifact_version: int
+ stage: LifecycleStage
+ role: AgentRole
+ role_title: str
+
+ quality_score: int
+ deterministic_score: int
+ band: str
+ verdict: ReviewVerdict
+
+ summary: str
+ strengths: list[ReviewFindingView] = Field(default_factory=list)
+ weaknesses: list[ReviewFindingView] = Field(default_factory=list)
+ suggestions: list[ReviewFindingView] = Field(default_factory=list)
+
+ reasoning_applied: bool
+ reviewer_provider: str | None = None
+ reviewer_model: str | None = None
+ created_at: datetime
+
+ @classmethod
+ def build(
+ cls, review: ArtifactReview, *, title: str = "", artifact_type: ArtifactType | None = None
+ ) -> ReviewView:
+ return cls(
+ id=review.id,
+ artifact_id=review.artifact_id,
+ artifact_title=title,
+ artifact_type=artifact_type,
+ artifact_version=review.artifact_version,
+ stage=review.stage,
+ role=review.role,
+ role_title=ROLE_TITLES[review.role],
+ quality_score=review.quality_score,
+ deterministic_score=review.deterministic_score,
+ band=review.band,
+ verdict=review.verdict,
+ summary=review.summary,
+ strengths=[ReviewFindingView(**item.model_dump()) for item in review.strengths],
+ weaknesses=[ReviewFindingView(**item.model_dump()) for item in review.weaknesses],
+ suggestions=[ReviewFindingView(**item.model_dump()) for item in review.suggestions],
+ reasoning_applied=review.reasoning_applied,
+ reviewer_provider=review.reviewer_provider,
+ reviewer_model=review.reviewer_model,
+ created_at=review.created_at,
+ )
+
+
+class RoleScore(BaseModel):
+ """A specialist's average score across everything it produced."""
+
+ role: AgentRole
+ role_title: str
+ average_score: int
+ artifacts_reviewed: int
+ lowest_score: int
+ needs_revision: int = Field(
+ default=0, description="How many of its artifacts a reviewer would send back."
+ )
+
+
+class ProjectReviewSummary(BaseModel):
+ """Everything the Helix Review view renders.
+
+ `10_UI_UX_Plan.md` asks the workspace to communicate engineering health at a
+ glance; this is that, scoped to quality rather than progress.
+ """
+
+ project_id: str
+ overall_score: int = Field(
+ default=0, description="Mean across every reviewed artifact. 0 when none exist."
+ )
+ artifacts_reviewed: int = 0
+ reasoning_coverage: int = Field(
+ default=0,
+ description=(
+ "Percentage of reviews a model contributed to. Below 100 means some "
+ "reviews are purely structural, which the view states plainly."
+ ),
+ )
+ needs_revision: int = 0
+ by_role: list[RoleScore] = Field(default_factory=list)
+ recommendations: list[ReviewFindingView] = Field(
+ default_factory=list,
+ description="Suggestions from the lowest-scoring artifacts, worth acting on first.",
+ )
+ reviews: list[ReviewView] = Field(default_factory=list)
+
+
+# `ArtifactDetail.review` is annotated before `ReviewView` exists, so the model
+# is rebuilt here — once the forward reference can actually be resolved.
+ArtifactDetail.model_rebuild()
diff --git a/submissions/Victorious/apps/api/app/api/views.py b/submissions/Victorious/apps/api/app/api/views.py
new file mode 100644
index 00000000..067a17b7
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/api/views.py
@@ -0,0 +1,459 @@
+"""Assembles API responses from shared memory.
+
+Routers stay thin: they parse a request, call one function here, and return the
+result. Response assembly lives in this module because it frequently needs
+several reads combined — an artifact list plus the staleness computed from the
+traceability graph — and putting that in a router would make it untestable
+without HTTP.
+"""
+
+from __future__ import annotations
+
+from app.api.schemas import (
+ AgentCard,
+ ApprovalView,
+ ArtifactDetail,
+ ArtifactSummary,
+ EventView,
+ ImpactedArtifactView,
+ ImpactPreview,
+ ProjectDetail,
+ ProjectReviewSummary,
+ ProjectSummary,
+ ReviewFindingView,
+ ReviewView,
+ RoleScore,
+ StageSummary,
+ TraceEdgeView,
+ TraceGraph,
+ TraceNode,
+ VersionSummary,
+)
+from app.domain.agents import AgentRun
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import (
+ ROLE_TITLES,
+ STAGE_OWNERS,
+ STAGE_SEQUENCE,
+ AgentRole,
+ LifecycleStage,
+ StageStatus,
+)
+from app.domain.reviews import ArtifactReview
+from app.domain.traceability import current_edges
+from app.memory.repository import SharedMemory
+
+#: Stages that represent work. ``IDEA`` is the state a project starts in.
+WORKING_STAGES = tuple(stage for stage in STAGE_SEQUENCE if stage is not LifecycleStage.IDEA)
+
+
+async def stale_ids(memory: SharedMemory, project_id: str) -> set[str]:
+ """Artifacts whose upstream has moved on since they were derived.
+
+ Computed from the graph on every read rather than stored (ADR-0007), so it
+ cannot disagree with reality.
+ """
+ entries = await memory.traces.stale_edges(project_id)
+ return {stale.edge.downstream_artifact_id for stale in entries}
+
+
+async def project_summary(memory: SharedMemory, project_id: str) -> ProjectSummary:
+ project = await memory.projects.get(project_id)
+ artifacts = await memory.artifacts.list_for_project(project_id)
+ pending = await memory.approvals.list_for_project(project_id, pending_only=True)
+
+ return ProjectSummary.build(
+ project,
+ artifact_count=len([a for a in artifacts if a.has_content]),
+ pending_approvals=len(pending),
+ total_stages=len(WORKING_STAGES),
+ )
+
+
+async def list_projects(memory: SharedMemory, *, limit: int = 50) -> list[ProjectSummary]:
+ projects = await memory.projects.list_all(limit=limit)
+
+ summaries = []
+ for project in projects:
+ artifacts = await memory.artifacts.list_for_project(project.id)
+ pending = await memory.approvals.list_for_project(project.id, pending_only=True)
+ summaries.append(
+ ProjectSummary.build(
+ project,
+ artifact_count=len([a for a in artifacts if a.has_content]),
+ pending_approvals=len(pending),
+ total_stages=len(WORKING_STAGES),
+ )
+ )
+ return summaries
+
+
+async def project_detail(memory: SharedMemory, project_id: str) -> ProjectDetail:
+ """A project with its complete stage timeline.
+
+ Every working stage appears, including ones not yet started —
+ `10_UI_UX_Plan.md` requires the timeline to show the whole lifecycle so a
+ user can see what happens next, not only what has happened.
+ """
+ project = await memory.projects.get(project_id)
+ artifacts = await memory.artifacts.list_for_project(project_id)
+ pending = await memory.approvals.list_for_project(project_id, pending_only=True)
+
+ counts: dict[LifecycleStage, int] = {}
+ for artifact in artifacts:
+ if artifact.has_content:
+ counts[artifact.stage] = counts.get(artifact.stage, 0) + 1
+
+ stages = []
+ for stage in WORKING_STAGES:
+ state = project.stage_state(stage)
+ role = STAGE_OWNERS.get(stage)
+ stages.append(
+ StageSummary(
+ stage=stage,
+ status=state.status if state else StageStatus.PENDING,
+ owner_role=role,
+ owner_title=ROLE_TITLES[role] if role else None,
+ started_at=state.started_at if state else None,
+ completed_at=state.completed_at if state else None,
+ artifact_count=counts.get(stage, 0),
+ )
+ )
+
+ summary = ProjectSummary.build(
+ project,
+ artifact_count=len([a for a in artifacts if a.has_content]),
+ pending_approvals=len(pending),
+ total_stages=len(WORKING_STAGES),
+ )
+ return ProjectDetail(**summary.model_dump(), stages=stages)
+
+
+async def list_artifacts(
+ memory: SharedMemory,
+ project_id: str,
+ *,
+ stage: LifecycleStage | None = None,
+ artifact_type: ArtifactType | None = None,
+) -> list[ArtifactSummary]:
+ artifacts = await memory.artifacts.list_for_project(
+ project_id, stage=stage, artifact_type=artifact_type
+ )
+ stale = await stale_ids(memory, project_id)
+
+ return [
+ ArtifactSummary.build(artifact, is_stale=artifact.id in stale)
+ for artifact in artifacts
+ if artifact.has_content
+ ]
+
+
+async def artifact_detail(
+ memory: SharedMemory, artifact_id: str, *, version: int | None = None
+) -> ArtifactDetail:
+ resolved = await memory.artifacts.get_version(artifact_id, version)
+ history = await memory.artifacts.list_versions(artifact_id)
+ stale = await stale_ids(memory, resolved.artifact.project_id)
+
+ # Reviews are written per version, so the review shown alongside a
+ # historical version is the one that version received — not the newest.
+ # A human revision produces a version no agent reviewed, which is why this
+ # is nullable rather than absent.
+ review = await memory.reviews.for_artifact(artifact_id, resolved.version.version)
+
+ return ArtifactDetail.from_resolved(
+ resolved,
+ is_stale=resolved.artifact.id in stale,
+ review=ReviewView.build(
+ review,
+ title=resolved.artifact.title,
+ artifact_type=resolved.artifact.type,
+ )
+ if review
+ else None,
+ versions=[
+ VersionSummary(
+ version=item.version,
+ summary=item.summary,
+ confidence=item.confidence,
+ produced_by_run_id=item.produced_by_run_id,
+ created_at=item.created_at,
+ )
+ for item in reversed(history)
+ ],
+ )
+
+
+async def organization(memory: SharedMemory, project_id: str) -> list[AgentCard]:
+ """Every specialist's current state, in lifecycle order.
+
+ Specialists that have not run yet appear as idle rather than being omitted:
+ `10_UI_UX_Plan.md` asks users to see the whole organization, and an agent
+ missing from the view is indistinguishable from one that does not exist.
+ """
+ runs = await memory.runs.list_for_project(project_id)
+
+ # Oldest first, so the last write per stage is the most recent run.
+ latest_by_stage: dict[LifecycleStage, AgentRun] = {
+ run.stage: run for run in sorted(runs, key=lambda item: item.started_at)
+ }
+
+ cards = []
+ for stage in WORKING_STAGES:
+ run = latest_by_stage.get(stage)
+ if run is not None:
+ cards.append(AgentCard.from_run(run))
+ elif (role := STAGE_OWNERS.get(stage)) is not None:
+ cards.append(AgentCard.idle(stage, role))
+
+ return cards
+
+
+async def list_approvals(
+ memory: SharedMemory, project_id: str, *, pending_only: bool = False
+) -> list[ApprovalView]:
+ project = await memory.projects.get(project_id)
+ requests = await memory.approvals.list_for_project(project_id, pending_only=pending_only)
+ artifacts = {
+ artifact.id: artifact
+ for artifact in await memory.artifacts.list_for_project(project_id)
+ }
+ stale = await stale_ids(memory, project_id)
+
+ views = []
+ for request in requests:
+ impacted = []
+ if request.impact is not None:
+ for item in request.impact.impacted:
+ artifact = artifacts.get(item.artifact_id)
+ impacted.append(
+ ImpactedArtifactView(
+ artifact_id=item.artifact_id,
+ title=artifact.title if artifact else item.artifact_id,
+ type=artifact.type if artifact else None,
+ depth=item.depth,
+ via_kind=item.via_kind.value,
+ )
+ )
+
+ views.append(
+ ApprovalView.build(
+ request,
+ project_name=project.name,
+ artifacts=[
+ ArtifactSummary.build(artifacts[aid], is_stale=aid in stale)
+ for aid in request.artifact_ids
+ if aid in artifacts
+ ],
+ impacted=impacted,
+ )
+ )
+ return views
+
+
+async def list_events(
+ memory: SharedMemory, project_id: str, *, limit: int = 200, after_id: str | None = None
+) -> list[EventView]:
+ events = await memory.events.list_for_project(project_id, limit=limit, after_id=after_id)
+ return [EventView.build(event) for event in events]
+
+
+async def impact_preview(
+ memory: SharedMemory, project_id: str, artifact_id: str
+) -> ImpactPreview:
+ """What would go out of date if this artifact changed."""
+ artifact = await memory.artifacts.get(artifact_id)
+ analysis = await memory.traces.analyse_impact(project_id, artifact_id)
+
+ artifacts = {
+ item.id: item for item in await memory.artifacts.list_for_project(project_id)
+ }
+
+ impacted = [
+ ImpactedArtifactView(
+ artifact_id=item.artifact_id,
+ title=artifacts[item.artifact_id].title
+ if item.artifact_id in artifacts
+ else item.artifact_id,
+ type=artifacts[item.artifact_id].type if item.artifact_id in artifacts else None,
+ depth=item.depth,
+ via_kind=item.via_kind.value,
+ )
+ for item in analysis.impacted
+ ]
+
+ stages = sorted(
+ {
+ artifacts[item.artifact_id].stage
+ for item in analysis.impacted
+ if item.artifact_id in artifacts
+ },
+ key=lambda stage: STAGE_SEQUENCE.index(stage),
+ )
+
+ return ImpactPreview(
+ artifact_id=artifact_id,
+ artifact_title=artifact.title,
+ impacted=impacted,
+ stages_affected=stages,
+ )
+
+
+async def trace_graph(memory: SharedMemory, project_id: str) -> TraceGraph:
+ """The full traceability graph, with staleness resolved per edge.
+
+ Both node and edge staleness are returned so the UI can render *why* an
+ artifact is stale — which specific derivation went out of date — rather than
+ only that it is.
+ """
+ artifacts = {
+ artifact.id: artifact
+ for artifact in await memory.artifacts.list_for_project(project_id)
+ if artifact.has_content
+ }
+ # Only the current declaration of each dependency. An agent that reruns adds
+ # a new edge rather than updating the old one, so the stored graph keeps a
+ # history of derivations; rendering all of them would draw the same
+ # dependency several times, each showing a different upstream version.
+ edges = current_edges(await memory.traces.list_for_project(project_id))
+ current = await memory.artifacts.current_versions(project_id)
+ stale_edge_ids = {
+ stale.edge.id: stale for stale in await memory.traces.stale_edges(project_id)
+ }
+ stale_nodes = {stale.edge.downstream_artifact_id for stale in stale_edge_ids.values()}
+
+ return TraceGraph(
+ project_id=project_id,
+ nodes=[
+ TraceNode(
+ id=artifact.id,
+ title=artifact.title,
+ type=artifact.type,
+ stage=artifact.stage,
+ role=artifact.owner_role,
+ version=artifact.current_version,
+ is_stale=artifact.id in stale_nodes,
+ )
+ for artifact in artifacts.values()
+ ],
+ edges=[
+ TraceEdgeView(
+ id=edge.id,
+ upstream_artifact_id=edge.upstream_artifact_id,
+ downstream_artifact_id=edge.downstream_artifact_id,
+ kind=edge.kind.value,
+ upstream_version=edge.upstream_version,
+ current_upstream_version=current.get(
+ edge.upstream_artifact_id, edge.upstream_version
+ ),
+ is_stale=edge.id in stale_edge_ids,
+ rationale=edge.rationale,
+ )
+ for edge in edges
+ # Edges to artifacts without content would render as dangling nodes.
+ if edge.upstream_artifact_id in artifacts
+ and edge.downstream_artifact_id in artifacts
+ ],
+ stale_artifact_ids=sorted(stale_nodes),
+ )
+
+
+#: How many recommendations a reader will actually act on.
+_MAX_RECOMMENDATIONS = 6
+
+
+def _recommendations(views: list[ReviewView]) -> list[ReviewFindingView]:
+ """The suggestions worth putting in front of a user, weakest artifact first.
+
+ Two rules, both about usefulness rather than completeness:
+
+ - **Specific before generic.** A reasoning-derived suggestion names something
+ in the artifact ("document the 409 conflict response"); a check-derived one
+ is a template ("expand with more detail"). The specific one is the one a
+ person can act on, so it is offered first.
+ - **No repeats.** A check fires the same sentence on every thin artifact, and
+ the same line three times reads as noise rather than emphasis.
+ """
+ weakest = sorted(views, key=lambda view: view.quality_score)
+
+ ordered = [
+ suggestion
+ for source in ("reasoning", "check")
+ for view in weakest
+ for suggestion in view.suggestions
+ if suggestion.source == source
+ ]
+
+ seen: set[str] = set()
+ unique: list[ReviewFindingView] = []
+ for suggestion in ordered:
+ if suggestion.text in seen:
+ continue
+ seen.add(suggestion.text)
+ unique.append(suggestion)
+
+ return unique[:_MAX_RECOMMENDATIONS]
+
+
+async def project_reviews(memory: SharedMemory, project_id: str) -> ProjectReviewSummary:
+ """Assemble the Helix Review view.
+
+ Recommendations are drawn from the lowest-scoring artifacts rather than from
+ everything: a list of every suggestion in the project is a backlog nobody
+ reads, while the three worst artifacts are a next action.
+ """
+ reviews = await memory.reviews.list_for_project(project_id)
+
+ if not reviews:
+ return ProjectReviewSummary(project_id=project_id)
+
+ artifacts = {
+ artifact.id: artifact
+ for artifact in await memory.artifacts.list_for_project(project_id)
+ }
+
+ views = [
+ ReviewView.build(
+ review,
+ title=artifacts[review.artifact_id].title
+ if review.artifact_id in artifacts
+ else review.artifact_id,
+ artifact_type=artifacts[review.artifact_id].type
+ if review.artifact_id in artifacts
+ else None,
+ )
+ for review in reviews
+ ]
+
+ by_role: dict[AgentRole, list[ArtifactReview]] = {}
+ for review in reviews:
+ by_role.setdefault(review.role, []).append(review)
+
+ role_scores = [
+ RoleScore(
+ role=role,
+ role_title=ROLE_TITLES[role],
+ average_score=round(sum(item.quality_score for item in group) / len(group)),
+ artifacts_reviewed=len(group),
+ lowest_score=min(item.quality_score for item in group),
+ needs_revision=sum(1 for item in group if not item.verdict.is_acceptable),
+ )
+ for role, group in sorted(by_role.items(), key=lambda entry: entry[0].value)
+ ]
+
+ recommendations = _recommendations(views)
+
+ reasoned = sum(1 for review in reviews if review.reasoning_applied)
+
+ return ProjectReviewSummary(
+ project_id=project_id,
+ overall_score=round(sum(item.quality_score for item in reviews) / len(reviews)),
+ artifacts_reviewed=len(reviews),
+ reasoning_coverage=round(100 * reasoned / len(reviews)),
+ needs_revision=sum(1 for item in reviews if not item.verdict.is_acceptable),
+ by_role=role_scores,
+ recommendations=recommendations,
+ # Newest first: a reader wants the most recent judgement, and the review
+ # history reads as a log.
+ reviews=sorted(views, key=lambda view: view.created_at, reverse=True),
+ )
diff --git a/submissions/Victorious/apps/api/app/core/__init__.py b/submissions/Victorious/apps/api/app/core/__init__.py
new file mode 100644
index 00000000..14ff03a7
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/__init__.py
@@ -0,0 +1,4 @@
+"""Cross-cutting infrastructure: configuration, logging, DI, errors, health.
+
+Usable by every layer, but owns no engineering domain logic itself.
+"""
diff --git a/submissions/Victorious/apps/api/app/core/bootstrap.py b/submissions/Victorious/apps/api/app/core/bootstrap.py
new file mode 100644
index 00000000..63360e29
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/bootstrap.py
@@ -0,0 +1,149 @@
+"""Application composition root.
+
+Every concrete implementation is chosen here and nowhere else. Modules depend on
+protocols; this file is the single place that says which implementation backs
+each one. Moving a swap decision into this file is what keeps the rest of the
+codebase free of conditional wiring.
+
+Each milestone extends ``build_container`` with its own registrations:
+Milestone 2 the LLM provider registry, Milestone 3 the orchestrator.
+"""
+
+from __future__ import annotations
+
+import time
+
+from app.agents.organization import build_organization
+from app.core.config import Settings
+from app.core.container import Container
+from app.core.health import ComponentHealth, HealthRegistry, HealthStatus
+from app.core.logging import get_logger
+from app.db.session import Database
+from app.events.bus import EventBus
+from app.llm.provider import LLMProvider
+from app.llm.registry import ProviderHealthCheck, build_provider
+from app.memory.context_builder import ContextBuilder
+from app.memory.health import DatabaseHealthCheck
+from app.memory.repository import SharedMemory
+from app.memory.sql_repository import SqlSharedMemory
+from app.orchestration.dispatcher import AgentDispatcher, RegistryDispatcher
+from app.orchestration.runner import OrchestrationRunner
+from app.review.reviewer import EngineeringReviewer
+
+logger = get_logger(__name__)
+
+
+class ProcessHealthCheck:
+ """Reports that the API process itself is serving requests.
+
+ Trivially healthy by construction — if it can answer, the process is alive —
+ but it carries uptime, which distinguishes a stable service from one caught
+ in a crash-restart loop.
+ """
+
+ def __init__(self) -> None:
+ self._started_at = time.monotonic()
+
+ @property
+ def name(self) -> str:
+ return "api"
+
+ @property
+ def critical(self) -> bool:
+ return True
+
+ async def check(self) -> ComponentHealth:
+ uptime = time.monotonic() - self._started_at
+ return ComponentHealth(
+ name=self.name,
+ status=HealthStatus.HEALTHY,
+ message=f"Serving for {uptime:.1f}s",
+ )
+
+
+def build_container(settings: Settings) -> Container:
+ """Construct and wire the application container.
+
+ Args:
+ settings: Resolved configuration. Passed explicitly rather than read from
+ the environment so tests can build a container against any config.
+
+ Returns:
+ A container with every protocol required at the current milestone bound
+ to an implementation.
+ """
+ container = Container()
+
+ container.register_instance(Settings, settings)
+
+ # --- Persistence and shared organizational memory ------------------------
+ # The Database singleton owns the engine; the container disposes it on
+ # shutdown through its `aclose` hook.
+ database = Database(settings.database)
+ container.register_instance(Database, database)
+
+ memory = SqlSharedMemory(database)
+ # Registered against the protocol, not the concrete class: this is the swap
+ # point ADR-0003 exists to preserve. Nothing downstream names SqlSharedMemory.
+ container.register_instance(SharedMemory, memory) # type: ignore[type-abstract]
+
+ container.register_singleton(
+ ContextBuilder,
+ lambda: ContextBuilder(memory.projects, memory.artifacts),
+ )
+
+ container.register_instance(EventBus, EventBus(memory.events))
+
+ # --- Reasoning -----------------------------------------------------------
+ # The provider swap point (ADR-0004). Nothing downstream names a vendor;
+ # changing VICTORIOUS_LLM__PROVIDER changes the backend for every agent.
+ provider = build_provider(settings.llm)
+ container.register_instance(LLMProvider, provider) # type: ignore[type-abstract]
+
+ # --- The engineering organization ----------------------------------------
+ context_builder = container.resolve(ContextBuilder)
+ events = container.resolve(EventBus)
+
+ # The reviewer shares the organization's provider, so it runs on recorded
+ # fixtures offline exactly as the agents do. Disabling review yields None,
+ # and every agent then behaves as it did before the layer existed.
+ reviewer = (
+ EngineeringReviewer(provider, settings.review) if settings.review.enabled else None
+ )
+ if reviewer is not None:
+ container.register_instance(EngineeringReviewer, reviewer)
+
+ dispatcher = RegistryDispatcher()
+ for agent in build_organization(memory, provider, context_builder, events, reviewer):
+ # Registration validates the agent's role against the domain's owner for
+ # its stage, so a mis-wired organization fails at startup rather than
+ # producing artifacts attributed to the wrong specialist.
+ dispatcher.register(agent)
+
+ container.register_instance(RegistryDispatcher, dispatcher)
+ container.register_instance(
+ AgentDispatcher, # type: ignore[type-abstract]
+ dispatcher,
+ )
+
+ container.register_singleton(
+ OrchestrationRunner,
+ lambda: OrchestrationRunner(memory, provider, events, dispatcher, settings.review),
+ )
+
+ # --- Health --------------------------------------------------------------
+ health_registry = HealthRegistry()
+ health_registry.register(ProcessHealthCheck())
+ health_registry.register(DatabaseHealthCheck(database))
+ health_registry.register(ProviderHealthCheck(provider, settings.llm))
+ container.register_instance(HealthRegistry, health_registry)
+
+ logger.info(
+ "Container built",
+ extra={
+ "environment": settings.environment.value,
+ "llm_provider": settings.llm.provider.value,
+ "vector_store_enabled": settings.vector_store.enabled,
+ },
+ )
+ return container
diff --git a/submissions/Victorious/apps/api/app/core/config.py b/submissions/Victorious/apps/api/app/core/config.py
new file mode 100644
index 00000000..13afa504
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/config.py
@@ -0,0 +1,193 @@
+"""Typed application configuration.
+
+Every runtime knob is declared here with a type and a default. Nothing in the
+codebase reads ``os.environ`` directly — configuration arrives through injection,
+which keeps modules testable and makes the full set of tunables discoverable in
+one file.
+
+Environment variables use the ``VICTORIOUS_`` prefix with ``__`` as the nesting
+delimiter, so the LLM provider is set via ``VICTORIOUS_LLM__PROVIDER``.
+"""
+
+from __future__ import annotations
+
+from enum import StrEnum
+from functools import lru_cache
+from typing import Annotated, Literal
+
+from pydantic import BaseModel, Field, field_validator
+from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
+
+
+class Environment(StrEnum):
+ """Deployment environment. Controls docs exposure and error verbosity."""
+
+ LOCAL = "local"
+ TEST = "test"
+ STAGING = "staging"
+ PRODUCTION = "production"
+
+
+class LLMProvider(StrEnum):
+ """Supported reasoning providers.
+
+ ``FIXTURE`` replays recorded responses from disk. It exists so the platform
+ can be demonstrated and tested with no network access and no API spend — see
+ ADR-0005. It is a first-class provider, not a mock.
+ """
+
+ ANTHROPIC = "anthropic"
+ GEMINI = "gemini"
+ FIXTURE = "fixture"
+
+
+class DatabaseSettings(BaseModel):
+ """Persistence configuration for the shared organizational memory."""
+
+ url: str = Field(
+ default="sqlite+aiosqlite:///./victorious.db",
+ description="SQLAlchemy async URL. PostgreSQL in compose, SQLite locally.",
+ )
+ echo: bool = Field(default=False, description="Log every emitted SQL statement.")
+ pool_size: int = Field(default=5, ge=1, le=50)
+ max_overflow: int = Field(default=10, ge=0, le=50)
+
+
+class LLMSettings(BaseModel):
+ """Reasoning provider configuration.
+
+ ``provider`` selects the default adapter; the abstraction in ``app.llm``
+ (Milestone 2) allows any agent to override it per invocation.
+ """
+
+ provider: LLMProvider = Field(default=LLMProvider.ANTHROPIC)
+ anthropic_api_key: str | None = Field(default=None, repr=False)
+ google_api_key: str | None = Field(default=None, repr=False)
+ anthropic_model: str = Field(default="claude-sonnet-5")
+ gemini_model: str = Field(default="gemini-2.5-pro")
+ fixture_dir: str = Field(
+ default="./fixtures",
+ description="Directory of recorded provider responses used by the fixture provider.",
+ )
+ record_fixtures: bool = Field(
+ default=False,
+ description=(
+ "Wrap the live provider in a recorder, writing every response to "
+ "`fixture_dir`. Run once against a real provider to produce the "
+ "offline demo corpus."
+ ),
+ )
+ max_retries: int = Field(default=3, ge=0, le=10)
+ timeout_seconds: float = Field(default=120.0, gt=0)
+
+
+class VectorStoreSettings(BaseModel):
+ """Semantic memory configuration.
+
+ Runs as an embedded persistent client rather than a separate service — see
+ ADR-0005.
+ """
+
+ enabled: bool = Field(default=False)
+ persist_dir: str = Field(default="./.chroma")
+ collection: str = Field(default="victorious_memory")
+
+
+class ReviewSettings(BaseModel):
+ """Engineering review configuration.
+
+ Reviewing is on by default because a score on every artifact is the point;
+ *blocking* on that score is off by default because a review that halts the
+ workflow is a new way for a live demonstration to stall. Turn it on
+ deliberately (`VICTORIOUS_REVIEW__BLOCKING=true`) to show the gate.
+ """
+
+ enabled: bool = Field(
+ default=True, description="Review each artifact as it is produced."
+ )
+ use_reasoning: bool = Field(
+ default=True,
+ description=(
+ "Let a model contribute prose and a bounded score adjustment. When "
+ "false, reviews are purely structural and say so."
+ ),
+ )
+ blocking: bool = Field(
+ default=False,
+ description=(
+ "Whether the Executive AI halts a stage whose upstream reviews fall "
+ "below `revision_threshold`. Advisory when false."
+ ),
+ )
+ revision_threshold: int = Field(
+ default=60, ge=0, le=100, description="Below this, the verdict is needs_revision."
+ )
+ strong_threshold: int = Field(
+ default=85, ge=0, le=100, description="At or above this, the verdict is approved."
+ )
+
+
+class ObservabilitySettings(BaseModel):
+ """Logging and diagnostics configuration."""
+
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
+ json_logs: bool = Field(
+ default=True,
+ description="Structured JSON output. Disable locally for readable console logs.",
+ )
+
+
+class Settings(BaseSettings):
+ """Root settings object, injected wherever configuration is needed."""
+
+ model_config = SettingsConfigDict(
+ env_prefix="VICTORIOUS_",
+ env_nested_delimiter="__",
+ env_file=(".env", "../../.env"),
+ env_file_encoding="utf-8",
+ extra="ignore",
+ )
+
+ app_name: str = "Project Victorious"
+ version: str = "0.1.0"
+ environment: Environment = Environment.LOCAL
+
+ api_prefix: str = "/api/v1"
+
+ # NoDecode suppresses pydantic-settings' default JSON decoding for complex
+ # types, so the validator below can accept a plain comma-separated string.
+ # Without it, `VICTORIOUS_CORS_ORIGINS=http://a,http://b` fails as invalid JSON.
+ cors_origins: Annotated[list[str], NoDecode] = Field(default=["http://localhost:3000"])
+
+ database: DatabaseSettings = Field(default_factory=DatabaseSettings)
+ llm: LLMSettings = Field(default_factory=LLMSettings)
+ vector_store: VectorStoreSettings = Field(default_factory=VectorStoreSettings)
+ review: ReviewSettings = Field(default_factory=ReviewSettings)
+ observability: ObservabilitySettings = Field(default_factory=ObservabilitySettings)
+
+ @field_validator("cors_origins", mode="before")
+ @classmethod
+ def _split_origins(cls, value: object) -> object:
+ """Accept a comma-separated string so a single env var can carry a list."""
+ if isinstance(value, str):
+ return [origin.strip() for origin in value.split(",") if origin.strip()]
+ return value
+
+ @property
+ def is_production(self) -> bool:
+ return self.environment is Environment.PRODUCTION
+
+ @property
+ def docs_url(self) -> str | None:
+ """OpenAPI docs are disabled in production to reduce surface area."""
+ return None if self.is_production else "/docs"
+
+
+@lru_cache(maxsize=1)
+def get_settings() -> Settings:
+ """Return the process-wide settings singleton.
+
+ Cached because settings are immutable for the lifetime of the process.
+ Tests clear the cache via ``get_settings.cache_clear()``.
+ """
+ return Settings()
diff --git a/submissions/Victorious/apps/api/app/core/container.py b/submissions/Victorious/apps/api/app/core/container.py
new file mode 100644
index 00000000..6f3a7802
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/container.py
@@ -0,0 +1,122 @@
+"""Dependency injection container.
+
+Components register against a *protocol* and resolve by that protocol, never by
+concrete type. That indirection is what makes the roadmap's swap points real
+rather than aspirational: the Anthropic provider can be exchanged for Gemini or
+the fixture replayer, and the SQL memory repository for any other implementation,
+without a single call site changing.
+
+Deliberately small. A full IoC framework would add magic and a dependency for
+behaviour that fits in a hundred lines, and FastAPI already owns request-scoped
+injection — this container owns *application*-scoped wiring.
+"""
+
+from __future__ import annotations
+
+import inspect
+from collections.abc import Callable
+from typing import Any, TypeVar
+
+from app.core.logging import get_logger
+
+logger = get_logger(__name__)
+
+T = TypeVar("T")
+
+
+class ContainerError(RuntimeError):
+ """Raised when a dependency is resolved that was never registered."""
+
+
+class Container:
+ """Application-scoped registry of protocol implementations.
+
+ Two lifetimes are supported:
+
+ - **singleton** — one instance for the process. The default, and correct for
+ stateless collaborators (providers, repositories, buses).
+ - **factory** — a fresh instance per resolution, for anything holding
+ per-use mutable state.
+ """
+
+ def __init__(self) -> None:
+ self._factories: dict[type, Callable[[], Any]] = {}
+ self._singletons: dict[type, Any] = {}
+ self._singleton_keys: set[type] = set()
+
+ def register_singleton(self, protocol: type[T], factory: Callable[[], T]) -> None:
+ """Register a lazily-constructed, cached implementation.
+
+ The factory runs on first resolution rather than at registration, so
+ startup does not pay for components a given process never uses.
+ """
+ self._factories[protocol] = factory
+ self._singleton_keys.add(protocol)
+ logger.debug("Registered singleton", extra={"protocol": protocol.__name__})
+
+ def register_instance(self, protocol: type[T], instance: T) -> None:
+ """Register an already-constructed implementation."""
+ self._singletons[protocol] = instance
+ self._singleton_keys.add(protocol)
+ logger.debug("Registered instance", extra={"protocol": protocol.__name__})
+
+ def register_factory(self, protocol: type[T], factory: Callable[[], T]) -> None:
+ """Register an implementation constructed fresh on every resolution."""
+ self._factories[protocol] = factory
+ self._singleton_keys.discard(protocol)
+ logger.debug("Registered factory", extra={"protocol": protocol.__name__})
+
+ def resolve(self, protocol: type[T]) -> T:
+ """Return the implementation registered for ``protocol``.
+
+ Raises:
+ ContainerError: if nothing is registered. Failing loudly at the call
+ site beats silently handing back ``None`` and failing later.
+ """
+ if protocol in self._singletons:
+ return self._singletons[protocol] # type: ignore[no-any-return]
+
+ factory = self._factories.get(protocol)
+ if factory is None:
+ raise ContainerError(
+ f"No implementation registered for {protocol.__name__}. "
+ "Register it in app.core.bootstrap before resolving."
+ )
+
+ instance = factory()
+ if protocol in self._singleton_keys:
+ self._singletons[protocol] = instance
+ return instance # type: ignore[no-any-return]
+
+ def has(self, protocol: type) -> bool:
+ """Return whether ``protocol`` has an implementation registered."""
+ return protocol in self._singletons or protocol in self._factories
+
+ async def aclose(self) -> None:
+ """Dispose every instantiated singleton that exposes a close hook.
+
+ Called on application shutdown so database engines, HTTP clients, and
+ provider sessions are released deterministically instead of at GC time.
+ """
+ for protocol, instance in self._singletons.items():
+ closer = getattr(instance, "aclose", None) or getattr(instance, "close", None)
+ if closer is None:
+ continue
+ try:
+ result = closer()
+ if inspect.isawaitable(result):
+ await result
+ # Broad by design: shutdown is best-effort, and one failing disposer
+ # must not prevent the remaining singletons from being released.
+ except Exception:
+ logger.exception(
+ "Failed to close dependency", extra={"protocol": protocol.__name__}
+ )
+
+ self._singletons.clear()
+
+ def clear(self) -> None:
+ """Reset all registrations. Used by tests to isolate wiring."""
+ self._factories.clear()
+ self._singletons.clear()
+ self._singleton_keys.clear()
diff --git a/submissions/Victorious/apps/api/app/core/errors.py b/submissions/Victorious/apps/api/app/core/errors.py
new file mode 100644
index 00000000..f7254adf
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/errors.py
@@ -0,0 +1,166 @@
+"""HTTP error envelope and exception handlers.
+
+This module is the *only* place that knows how a domain error becomes an HTTP
+response. Domain and orchestration code raises ``VictoriousError`` subclasses and
+stays entirely unaware of status codes.
+
+Every error response shares one shape, so the frontend needs a single error
+renderer rather than per-endpoint handling:
+
+ {
+ "error": {
+ "code": "dependency_not_satisfied",
+ "message": "Architecture stage requires approved requirements",
+ "details": {"missing": ["requirements"]},
+ "correlation_id": "3f9a..."
+ }
+ }
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from fastapi import FastAPI, Request, status
+from fastapi.encoders import jsonable_encoder
+from fastapi.exceptions import RequestValidationError
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel, Field
+from starlette.exceptions import HTTPException as StarletteHTTPException
+
+from app.core.logging import get_correlation_id, get_logger
+from app.domain.errors import (
+ ApprovalRequiredError,
+ ConflictError,
+ DependencyNotSatisfiedError,
+ NotFoundError,
+ ProviderError,
+ ValidationError,
+ VictoriousError,
+)
+
+logger = get_logger(__name__)
+
+# Single source of truth for domain-error -> HTTP-status mapping. Adding a new
+# domain error without an entry here yields 500, which is the correct default:
+# an unmapped error is a genuine oversight, not something to guess at.
+_STATUS_BY_ERROR: dict[type[VictoriousError], int] = {
+ NotFoundError: status.HTTP_404_NOT_FOUND,
+ ValidationError: status.HTTP_422_UNPROCESSABLE_CONTENT,
+ ConflictError: status.HTTP_409_CONFLICT,
+ DependencyNotSatisfiedError: status.HTTP_409_CONFLICT,
+ ApprovalRequiredError: status.HTTP_403_FORBIDDEN,
+ ProviderError: status.HTTP_502_BAD_GATEWAY,
+}
+
+
+class ErrorDetail(BaseModel):
+ """Body of an error response."""
+
+ code: str = Field(description="Stable machine-readable error identifier.")
+ message: str = Field(description="Human-readable description.")
+ details: dict[str, Any] = Field(default_factory=dict)
+ correlation_id: str | None = Field(
+ default=None,
+ description="Ties this response to the server logs for the same request.",
+ )
+
+
+class ErrorResponse(BaseModel):
+ """Envelope returned for every non-2xx response."""
+
+ error: ErrorDetail
+
+
+def _render(status_code: int, detail: ErrorDetail) -> JSONResponse:
+ return JSONResponse(
+ status_code=status_code,
+ content=jsonable_encoder(ErrorResponse(error=detail)),
+ )
+
+
+def _status_for(exc: VictoriousError) -> int:
+ """Resolve a status code, honouring subclass relationships."""
+ for error_type, code in _STATUS_BY_ERROR.items():
+ if isinstance(exc, error_type):
+ return code
+ return status.HTTP_500_INTERNAL_SERVER_ERROR
+
+
+async def _handle_domain_error(request: Request, exc: Exception) -> JSONResponse:
+ assert isinstance(exc, VictoriousError) # noqa: S101 - handler registered by type
+ status_code = _status_for(exc)
+
+ # 5xx means the platform misbehaved and deserves a stack trace; 4xx is the
+ # caller's problem and would only add noise at error level.
+ log = logger.exception if status_code >= 500 else logger.warning
+ log(
+ "Request failed: %s",
+ exc.code,
+ extra={"path": request.url.path, "error_code": exc.code, "status_code": status_code},
+ )
+
+ return _render(
+ status_code,
+ ErrorDetail(
+ code=exc.code,
+ message=exc.message,
+ details=exc.details,
+ correlation_id=get_correlation_id(),
+ ),
+ )
+
+
+async def _handle_request_validation(request: Request, exc: Exception) -> JSONResponse:
+ """Reshape FastAPI's validation errors into the common envelope."""
+ assert isinstance(exc, RequestValidationError) # noqa: S101
+ return _render(
+ status.HTTP_422_UNPROCESSABLE_CONTENT,
+ ErrorDetail(
+ code="request_validation_error",
+ message="The request payload failed validation.",
+ details={"errors": jsonable_encoder(exc.errors())},
+ correlation_id=get_correlation_id(),
+ ),
+ )
+
+
+async def _handle_http_exception(request: Request, exc: Exception) -> JSONResponse:
+ """Wrap Starlette's built-in HTTP errors (404 routing, 405, ...)."""
+ assert isinstance(exc, StarletteHTTPException) # noqa: S101
+ return _render(
+ exc.status_code,
+ ErrorDetail(
+ code=f"http_{exc.status_code}",
+ message=str(exc.detail),
+ correlation_id=get_correlation_id(),
+ ),
+ )
+
+
+async def _handle_unexpected(request: Request, exc: Exception) -> JSONResponse:
+ """Last-resort handler.
+
+ The exception message is deliberately withheld from the client — it may carry
+ internal detail — while the correlation ID gives an operator an exact path to
+ the logged stack trace.
+ """
+ logger.exception(
+ "Unhandled exception", extra={"path": request.url.path, "error_type": type(exc).__name__}
+ )
+ return _render(
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ ErrorDetail(
+ code="internal_error",
+ message="An unexpected error occurred.",
+ correlation_id=get_correlation_id(),
+ ),
+ )
+
+
+def register_exception_handlers(app: FastAPI) -> None:
+ """Wire every handler onto the application."""
+ app.add_exception_handler(VictoriousError, _handle_domain_error)
+ app.add_exception_handler(RequestValidationError, _handle_request_validation)
+ app.add_exception_handler(StarletteHTTPException, _handle_http_exception)
+ app.add_exception_handler(Exception, _handle_unexpected)
diff --git a/submissions/Victorious/apps/api/app/core/health.py b/submissions/Victorious/apps/api/app/core/health.py
new file mode 100644
index 00000000..2767430a
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/health.py
@@ -0,0 +1,155 @@
+"""Component health checking.
+
+Liveness and readiness are answered separately, because they mean different
+things to an orchestrator: liveness failing means *restart me*, readiness failing
+means *stop sending me traffic until my dependencies recover*.
+
+Readiness is assembled from a registry of ``HealthCheck`` implementations. Each
+later milestone contributes its own check — the memory repository in Milestone 1,
+the LLM providers in Milestone 2 — without this module changing. That is the
+extensibility requirement applied to observability rather than only to agents.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import time
+from enum import StrEnum
+from typing import Protocol, runtime_checkable
+
+from pydantic import BaseModel, Field
+
+from app.core.logging import get_logger
+
+logger = get_logger(__name__)
+
+# A slow dependency must not turn a readiness probe into a hanging request.
+_CHECK_TIMEOUT_SECONDS = 5.0
+
+
+class HealthStatus(StrEnum):
+ """Health of a single component or of the system as a whole."""
+
+ HEALTHY = "healthy"
+ DEGRADED = "degraded"
+ UNHEALTHY = "unhealthy"
+
+
+class ComponentHealth(BaseModel):
+ """Result of checking one component."""
+
+ name: str
+ status: HealthStatus
+ message: str | None = None
+ latency_ms: float | None = Field(default=None, ge=0)
+
+
+@runtime_checkable
+class HealthCheck(Protocol):
+ """A dependency that can report whether it is usable.
+
+ Implementations must not raise: a check that throws is treated as unhealthy,
+ but returning a descriptive ``ComponentHealth`` produces a far better
+ operator experience than an exception trace.
+ """
+
+ @property
+ def name(self) -> str:
+ """Stable component identifier, e.g. ``"database"``."""
+ ...
+
+ @property
+ def critical(self) -> bool:
+ """Whether failure makes the whole system unready.
+
+ Non-critical failures degrade rather than fail readiness — the vector
+ store being down should not take the platform offline.
+ """
+ ...
+
+ async def check(self) -> ComponentHealth:
+ """Probe the component and describe its state."""
+ ...
+
+
+class HealthReport(BaseModel):
+ """Aggregate readiness across every registered component."""
+
+ status: HealthStatus
+ version: str
+ environment: str
+ components: list[ComponentHealth] = Field(default_factory=list)
+
+ @property
+ def is_ready(self) -> bool:
+ """Degraded still serves traffic; unhealthy does not."""
+ return self.status is not HealthStatus.UNHEALTHY
+
+
+class HealthRegistry:
+ """Collects health checks and evaluates them concurrently."""
+
+ def __init__(self) -> None:
+ self._checks: list[HealthCheck] = []
+
+ def register(self, check: HealthCheck) -> None:
+ """Add a component check to the readiness probe."""
+ self._checks.append(check)
+ logger.debug("Registered health check", extra={"component": check.name})
+
+ async def evaluate(self, *, version: str, environment: str) -> HealthReport:
+ """Run every check concurrently and aggregate the outcome.
+
+ Concurrency matters: readiness probes are polled frequently, and running
+ N checks in series would multiply probe latency by N.
+ """
+ results = await asyncio.gather(
+ *(self._run_one(check) for check in self._checks),
+ return_exceptions=False,
+ )
+
+ critical_by_name = {check.name: check.critical for check in self._checks}
+ overall = HealthStatus.HEALTHY
+
+ for result in results:
+ if result.status is HealthStatus.HEALTHY:
+ continue
+ if critical_by_name.get(result.name, True) and result.status is HealthStatus.UNHEALTHY:
+ overall = HealthStatus.UNHEALTHY
+ break
+ overall = HealthStatus.DEGRADED
+
+ return HealthReport(
+ status=overall,
+ version=version,
+ environment=environment,
+ components=list(results),
+ )
+
+ async def _run_one(self, check: HealthCheck) -> ComponentHealth:
+ """Execute one check under a timeout, converting failures into results."""
+ started = time.perf_counter()
+ try:
+ async with asyncio.timeout(_CHECK_TIMEOUT_SECONDS):
+ result = await check.check()
+ if result.latency_ms is None:
+ result = result.model_copy(
+ update={"latency_ms": (time.perf_counter() - started) * 1000}
+ )
+ return result
+ except TimeoutError:
+ return ComponentHealth(
+ name=check.name,
+ status=HealthStatus.UNHEALTHY,
+ message=f"Health check exceeded {_CHECK_TIMEOUT_SECONDS:.0f}s",
+ latency_ms=(time.perf_counter() - started) * 1000,
+ )
+ # Broad by design: a health probe reports failure, it never propagates it.
+ except Exception as exc:
+ logger.exception("Health check raised", extra={"component": check.name})
+ return ComponentHealth(
+ name=check.name,
+ status=HealthStatus.UNHEALTHY,
+ message=f"{type(exc).__name__}: {exc}",
+ latency_ms=(time.perf_counter() - started) * 1000,
+ )
diff --git a/submissions/Victorious/apps/api/app/core/logging.py b/submissions/Victorious/apps/api/app/core/logging.py
new file mode 100644
index 00000000..7b6e5347
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/logging.py
@@ -0,0 +1,118 @@
+"""Structured logging with request correlation.
+
+Every log line carries a ``correlation_id`` that ties it to the originating HTTP
+request. Once the orchestration layer lands in Milestone 3, the same identifier
+propagates through agent runs, so a single engineering decision can be traced
+from the API call that triggered it through every agent that contributed to it.
+
+That property is the logging half of the traceability guarantee the platform is
+built around; the data half lives in the memory model.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import sys
+import uuid
+from collections.abc import Iterator
+from contextlib import contextmanager
+from contextvars import ContextVar
+from typing import Any
+
+from app.core.config import ObservabilitySettings
+
+_correlation_id: ContextVar[str | None] = ContextVar("correlation_id", default=None)
+
+# Attributes present on every LogRecord. Anything outside this set was supplied
+# by the caller via ``extra=`` and is therefore promoted into the JSON payload.
+_RESERVED_ATTRS = frozenset(
+ {
+ "args", "asctime", "created", "exc_info", "exc_text", "filename",
+ "funcName", "levelname", "levelno", "lineno", "module", "msecs",
+ "message", "msg", "name", "pathname", "process", "processName",
+ "relativeCreated", "stack_info", "thread", "threadName", "taskName",
+ }
+)
+
+
+def get_correlation_id() -> str | None:
+ """Return the correlation ID bound to the current context, if any."""
+ return _correlation_id.get()
+
+
+@contextmanager
+def correlation_context(correlation_id: str | None = None) -> Iterator[str]:
+ """Bind a correlation ID for the duration of the block.
+
+ Generates one when not supplied, so background work started outside a request
+ is still traceable.
+ """
+ resolved = correlation_id or str(uuid.uuid4())
+ token = _correlation_id.set(resolved)
+ try:
+ yield resolved
+ finally:
+ _correlation_id.reset(token)
+
+
+class JSONFormatter(logging.Formatter):
+ """Render records as single-line JSON for log aggregation."""
+
+ def format(self, record: logging.LogRecord) -> str:
+ payload: dict[str, Any] = {
+ "timestamp": self.formatTime(record, datefmt="%Y-%m-%dT%H:%M:%S%z"),
+ "level": record.levelname,
+ "logger": record.name,
+ "message": record.getMessage(),
+ }
+
+ if correlation_id := get_correlation_id():
+ payload["correlation_id"] = correlation_id
+
+ for key, value in record.__dict__.items():
+ if key not in _RESERVED_ATTRS and not key.startswith("_"):
+ payload[key] = value
+
+ if record.exc_info:
+ payload["exception"] = self.formatException(record.exc_info)
+
+ return json.dumps(payload, default=str)
+
+
+class ConsoleFormatter(logging.Formatter):
+ """Human-readable output for local development."""
+
+ def format(self, record: logging.LogRecord) -> str:
+ base = f"{record.levelname:<8} {record.name:<28} {record.getMessage()}"
+ if correlation_id := get_correlation_id():
+ base = f"[{correlation_id[:8]}] {base}"
+ if record.exc_info:
+ base = f"{base}\n{self.formatException(record.exc_info)}"
+ return base
+
+
+def configure_logging(settings: ObservabilitySettings) -> None:
+ """Install the root logging configuration.
+
+ Idempotent: existing handlers are replaced, so repeated calls in tests or
+ under a reloading server do not duplicate output.
+ """
+ handler = logging.StreamHandler(sys.stdout)
+ handler.setFormatter(JSONFormatter() if settings.json_logs else ConsoleFormatter())
+
+ root = logging.getLogger()
+ root.handlers.clear()
+ root.addHandler(handler)
+ root.setLevel(settings.log_level)
+
+ # Uvicorn installs its own handlers; defer to ours so every line is structured.
+ for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
+ uvicorn_logger = logging.getLogger(name)
+ uvicorn_logger.handlers.clear()
+ uvicorn_logger.propagate = True
+
+
+def get_logger(name: str) -> logging.Logger:
+ """Return a module-scoped logger. Preferred over ``logging.getLogger``."""
+ return logging.getLogger(name)
diff --git a/submissions/Victorious/apps/api/app/core/middleware.py b/submissions/Victorious/apps/api/app/core/middleware.py
new file mode 100644
index 00000000..c9e44aef
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/core/middleware.py
@@ -0,0 +1,71 @@
+"""HTTP middleware.
+
+Correlation IDs are established here, at the outermost layer, so every log line
+emitted while handling a request — including those from agents invoked deep in
+the orchestration graph — carries the same identifier.
+"""
+
+from __future__ import annotations
+
+import time
+from collections.abc import Awaitable, Callable
+
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.requests import Request
+from starlette.responses import Response
+
+from app.core.logging import correlation_context, get_logger
+
+logger = get_logger(__name__)
+
+CORRELATION_HEADER = "X-Correlation-ID"
+
+# Probe endpoints are polled continuously; logging them buries real traffic.
+_UNLOGGED_PATHS = frozenset({"/health", "/health/live", "/health/ready"})
+
+
+class CorrelationMiddleware(BaseHTTPMiddleware):
+ """Bind a correlation ID to the request context and echo it to the client.
+
+ An inbound ``X-Correlation-ID`` is honoured so a trace can span the browser,
+ the API, and any downstream service; otherwise one is generated.
+ """
+
+ async def dispatch(
+ self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
+ ) -> Response:
+ inbound = request.headers.get(CORRELATION_HEADER)
+
+ with correlation_context(inbound) as correlation_id:
+ request.state.correlation_id = correlation_id
+ response = await call_next(request)
+ response.headers[CORRELATION_HEADER] = correlation_id
+ return response
+
+
+class AccessLogMiddleware(BaseHTTPMiddleware):
+ """Emit one structured line per request with method, path, status, duration."""
+
+ async def dispatch(
+ self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
+ ) -> Response:
+ if request.url.path in _UNLOGGED_PATHS:
+ return await call_next(request)
+
+ started = time.perf_counter()
+ response = await call_next(request)
+ duration_ms = (time.perf_counter() - started) * 1000
+
+ logger.info(
+ "%s %s -> %s",
+ request.method,
+ request.url.path,
+ response.status_code,
+ extra={
+ "http_method": request.method,
+ "http_path": request.url.path,
+ "http_status": response.status_code,
+ "duration_ms": round(duration_ms, 2),
+ },
+ )
+ return response
diff --git a/submissions/Victorious/apps/api/app/db/__init__.py b/submissions/Victorious/apps/api/app/db/__init__.py
new file mode 100644
index 00000000..bbf4f71c
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/__init__.py
@@ -0,0 +1,29 @@
+"""Persistence layer: SQLAlchemy models, engine, and session lifecycle.
+
+Depended upon by ``app.memory``, and by nothing else. No agent, orchestrator, or
+router imports from here.
+"""
+
+from app.db.models import (
+ AgentRunRow,
+ ApprovalRow,
+ ArtifactRow,
+ ArtifactVersionRow,
+ Base,
+ EventRow,
+ ProjectRow,
+ TraceEdgeRow,
+)
+from app.db.session import Database
+
+__all__ = [
+ "AgentRunRow",
+ "ApprovalRow",
+ "ArtifactRow",
+ "ArtifactVersionRow",
+ "Base",
+ "Database",
+ "EventRow",
+ "ProjectRow",
+ "TraceEdgeRow",
+]
diff --git a/submissions/Victorious/apps/api/app/db/migrations/env.py b/submissions/Victorious/apps/api/app/db/migrations/env.py
new file mode 100644
index 00000000..dd682e7c
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/migrations/env.py
@@ -0,0 +1,89 @@
+"""Alembic migration environment.
+
+Reads the database URL from application settings rather than ``alembic.ini`` so
+migrations always target the same database the application does — one source of
+truth for the connection string, as for everything else.
+
+Runs against the async engine directly, so the same driver (aiosqlite or asyncpg)
+is exercised in migration as at runtime.
+"""
+
+from __future__ import annotations
+
+import asyncio
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy.engine import Connection
+from sqlalchemy.ext.asyncio import async_engine_from_config
+from sqlalchemy.pool import NullPool
+
+from app.core.config import get_settings
+from app.db.models import Base
+
+config = context.config
+
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# Autogenerate compares the live database against this metadata.
+target_metadata = Base.metadata
+
+config.set_main_option("sqlalchemy.url", get_settings().database.url)
+
+
+def _configure(connection: Connection) -> None:
+ """Apply options shared by online and offline modes."""
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata,
+ compare_type=True,
+ # SQLite cannot ALTER most columns; batch mode rewrites the table
+ # instead, so the same migration script runs on SQLite and PostgreSQL
+ # alike (ADR-0005).
+ render_as_batch=connection.dialect.name == "sqlite",
+ )
+
+
+def run_migrations_offline() -> None:
+ """Emit SQL to stdout without connecting.
+
+ Used to hand a reviewable script to a DBA rather than applying changes
+ directly to a production database.
+ """
+ context.configure(
+ url=config.get_main_option("sqlalchemy.url"),
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ compare_type=True,
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def _run_migrations(connection: Connection) -> None:
+ _configure(connection)
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+async def run_migrations_online() -> None:
+ """Apply migrations against the configured database."""
+ engine = async_engine_from_config(
+ config.get_section(config.config_ini_section, {}),
+ prefix="sqlalchemy.",
+ poolclass=NullPool,
+ )
+
+ async with engine.connect() as connection:
+ await connection.run_sync(_run_migrations)
+
+ await engine.dispose()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ asyncio.run(run_migrations_online())
diff --git a/submissions/Victorious/apps/api/app/db/migrations/script.py.mako b/submissions/Victorious/apps/api/app/db/migrations/script.py.mako
new file mode 100644
index 00000000..10f7ce0b
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/migrations/script.py.mako
@@ -0,0 +1,26 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Created: ${create_date}
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+${imports if imports else ""}
+revision: str = ${repr(up_revision)}
+down_revision: str | None = ${repr(down_revision)}
+branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
+depends_on: str | Sequence[str] | None = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/submissions/Victorious/apps/api/app/db/migrations/versions/20260807_1552_initial_schema.py b/submissions/Victorious/apps/api/app/db/migrations/versions/20260807_1552_initial_schema.py
new file mode 100644
index 00000000..7c1018fd
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/migrations/versions/20260807_1552_initial_schema.py
@@ -0,0 +1,181 @@
+"""initial schema
+
+Revision ID: 13ecbc9568b5
+Revises:
+Created: 2026-08-07 15:52:56.630858
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = '13ecbc9568b5'
+down_revision: str | None = None
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('projects',
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('name', sa.String(length=200), nullable=False),
+ sa.Column('description', sa.Text(), nullable=False),
+ sa.Column('current_stage', sa.String(length=50), nullable=False),
+ sa.Column('stages', sa.JSON(), nullable=False),
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
+ sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('agent_runs',
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('project_id', sa.String(length=64), nullable=False),
+ sa.Column('role', sa.String(length=50), nullable=False),
+ sa.Column('stage', sa.String(length=50), nullable=False),
+ sa.Column('status', sa.String(length=30), nullable=False),
+ sa.Column('task', sa.Text(), nullable=False),
+ sa.Column('reasoning_summary', sa.Text(), nullable=False),
+ sa.Column('confidence', sa.Float(), nullable=True),
+ sa.Column('input_artifact_ids', sa.JSON(), nullable=False),
+ sa.Column('output_artifact_ids', sa.JSON(), nullable=False),
+ sa.Column('blocked_on', sa.JSON(), nullable=False),
+ sa.Column('provider', sa.String(length=50), nullable=True),
+ sa.Column('model', sa.String(length=100), nullable=True),
+ sa.Column('input_tokens', sa.Integer(), nullable=False),
+ sa.Column('output_tokens', sa.Integer(), nullable=False),
+ sa.Column('correlation_id', sa.String(length=64), nullable=True),
+ sa.Column('error', sa.Text(), nullable=True),
+ sa.Column('started_at', sa.DateTime(timezone=True), nullable=False),
+ sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
+ sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ with op.batch_alter_table('agent_runs', schema=None) as batch_op:
+ batch_op.create_index('ix_runs_project_role', ['project_id', 'role'], unique=False)
+ batch_op.create_index('ix_runs_project_started', ['project_id', 'started_at'], unique=False)
+
+ op.create_table('approvals',
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('project_id', sa.String(length=64), nullable=False),
+ sa.Column('kind', sa.String(length=50), nullable=False),
+ sa.Column('stage', sa.String(length=50), nullable=False),
+ sa.Column('title', sa.String(length=300), nullable=False),
+ sa.Column('what_changed', sa.Text(), nullable=False),
+ sa.Column('why', sa.Text(), nullable=False),
+ sa.Column('requested_by', sa.String(length=50), nullable=False),
+ sa.Column('agents_involved', sa.JSON(), nullable=False),
+ sa.Column('artifact_ids', sa.JSON(), nullable=False),
+ sa.Column('impact', sa.JSON(), nullable=True),
+ sa.Column('status', sa.String(length=30), nullable=False),
+ sa.Column('feedback', sa.Text(), nullable=True),
+ sa.Column('decided_at', sa.DateTime(timezone=True), nullable=True),
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ with op.batch_alter_table('approvals', schema=None) as batch_op:
+ batch_op.create_index('ix_approvals_project_status', ['project_id', 'status'], unique=False)
+
+ op.create_table('artifacts',
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('project_id', sa.String(length=64), nullable=False),
+ sa.Column('type', sa.String(length=50), nullable=False),
+ sa.Column('title', sa.String(length=300), nullable=False),
+ sa.Column('stage', sa.String(length=50), nullable=False),
+ sa.Column('owner_role', sa.String(length=50), nullable=False),
+ sa.Column('status', sa.String(length=30), nullable=False),
+ sa.Column('current_version', sa.Integer(), nullable=False),
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
+ sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ with op.batch_alter_table('artifacts', schema=None) as batch_op:
+ batch_op.create_index('ix_artifacts_project_stage', ['project_id', 'stage'], unique=False)
+ batch_op.create_index('ix_artifacts_project_type', ['project_id', 'type'], unique=False)
+
+ op.create_table('events',
+ sa.Column('seq', sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('project_id', sa.String(length=64), nullable=False),
+ sa.Column('type', sa.String(length=50), nullable=False),
+ sa.Column('stage', sa.String(length=50), nullable=True),
+ sa.Column('role', sa.String(length=50), nullable=True),
+ sa.Column('summary', sa.Text(), nullable=False),
+ sa.Column('payload', sa.JSON(), nullable=False),
+ sa.Column('correlation_id', sa.String(length=64), nullable=True),
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('seq'),
+ sa.UniqueConstraint('id')
+ )
+ with op.batch_alter_table('events', schema=None) as batch_op:
+ batch_op.create_index('ix_events_project_seq', ['project_id', 'seq'], unique=False)
+
+ op.create_table('trace_edges',
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('project_id', sa.String(length=64), nullable=False),
+ sa.Column('upstream_artifact_id', sa.String(length=64), nullable=False),
+ sa.Column('downstream_artifact_id', sa.String(length=64), nullable=False),
+ sa.Column('kind', sa.String(length=30), nullable=False),
+ sa.Column('upstream_version', sa.Integer(), nullable=False),
+ sa.Column('created_by_run_id', sa.String(length=64), nullable=True),
+ sa.Column('rationale', sa.Text(), nullable=False),
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ with op.batch_alter_table('trace_edges', schema=None) as batch_op:
+ batch_op.create_index('ix_trace_downstream', ['downstream_artifact_id'], unique=False)
+ batch_op.create_index('ix_trace_project', ['project_id'], unique=False)
+ batch_op.create_index('ix_trace_upstream', ['upstream_artifact_id'], unique=False)
+
+ op.create_table('artifact_versions',
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('artifact_id', sa.String(length=64), nullable=False),
+ sa.Column('version', sa.Integer(), nullable=False),
+ sa.Column('body_markdown', sa.Text(), nullable=False),
+ sa.Column('content', sa.JSON(), nullable=False),
+ sa.Column('produced_by_run_id', sa.String(length=64), nullable=True),
+ sa.Column('summary', sa.Text(), nullable=False),
+ sa.Column('confidence', sa.Float(), nullable=True),
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(['artifact_id'], ['artifacts.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('artifact_id', 'version', name='uq_artifact_version')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('artifact_versions')
+ with op.batch_alter_table('trace_edges', schema=None) as batch_op:
+ batch_op.drop_index('ix_trace_upstream')
+ batch_op.drop_index('ix_trace_project')
+ batch_op.drop_index('ix_trace_downstream')
+
+ op.drop_table('trace_edges')
+ with op.batch_alter_table('events', schema=None) as batch_op:
+ batch_op.drop_index('ix_events_project_seq')
+
+ op.drop_table('events')
+ with op.batch_alter_table('artifacts', schema=None) as batch_op:
+ batch_op.drop_index('ix_artifacts_project_type')
+ batch_op.drop_index('ix_artifacts_project_stage')
+
+ op.drop_table('artifacts')
+ with op.batch_alter_table('approvals', schema=None) as batch_op:
+ batch_op.drop_index('ix_approvals_project_status')
+
+ op.drop_table('approvals')
+ with op.batch_alter_table('agent_runs', schema=None) as batch_op:
+ batch_op.drop_index('ix_runs_project_started')
+ batch_op.drop_index('ix_runs_project_role')
+
+ op.drop_table('agent_runs')
+ op.drop_table('projects')
+ # ### end Alembic commands ###
diff --git a/submissions/Victorious/apps/api/app/db/migrations/versions/20260807_2210_agent_approval_request_fields.py b/submissions/Victorious/apps/api/app/db/migrations/versions/20260807_2210_agent_approval_request_fields.py
new file mode 100644
index 00000000..84c8fa9b
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/migrations/versions/20260807_2210_agent_approval_request_fields.py
@@ -0,0 +1,36 @@
+"""agent approval request fields
+
+Revision ID: f9cd5a1567cf
+Revises: 13ecbc9568b5
+Created: 2026-08-07 22:10:47.502022
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = 'f9cd5a1567cf'
+down_revision: str | None = '13ecbc9568b5'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('agent_runs', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('requires_approval', sa.Boolean(), nullable=False))
+ batch_op.add_column(sa.Column('approval_reason', sa.Text(), nullable=False))
+
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('agent_runs', schema=None) as batch_op:
+ batch_op.drop_column('approval_reason')
+ batch_op.drop_column('requires_approval')
+
+ # ### end Alembic commands ###
diff --git a/submissions/Victorious/apps/api/app/db/migrations/versions/20260808_0013_artifact_reviews.py b/submissions/Victorious/apps/api/app/db/migrations/versions/20260808_0013_artifact_reviews.py
new file mode 100644
index 00000000..67309a9b
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/migrations/versions/20260808_0013_artifact_reviews.py
@@ -0,0 +1,59 @@
+"""artifact reviews
+
+Revision ID: fd8e2d9afd05
+Revises: f9cd5a1567cf
+Created: 2026-08-08 00:13:19.703522
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = 'fd8e2d9afd05'
+down_revision: str | None = 'f9cd5a1567cf'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('artifact_reviews',
+ sa.Column('id', sa.String(length=64), nullable=False),
+ sa.Column('project_id', sa.String(length=64), nullable=False),
+ sa.Column('artifact_id', sa.String(length=64), nullable=False),
+ sa.Column('artifact_version', sa.Integer(), nullable=False),
+ sa.Column('stage', sa.String(length=50), nullable=False),
+ sa.Column('role', sa.String(length=50), nullable=False),
+ sa.Column('produced_by_run_id', sa.String(length=64), nullable=True),
+ sa.Column('quality_score', sa.Integer(), nullable=False),
+ sa.Column('verdict', sa.String(length=40), nullable=False),
+ sa.Column('summary', sa.Text(), nullable=False),
+ sa.Column('strengths', sa.JSON(), nullable=False),
+ sa.Column('weaknesses', sa.JSON(), nullable=False),
+ sa.Column('suggestions', sa.JSON(), nullable=False),
+ sa.Column('deterministic_score', sa.Integer(), nullable=False),
+ sa.Column('reasoning_applied', sa.Boolean(), nullable=False),
+ sa.Column('reviewer_provider', sa.String(length=50), nullable=True),
+ sa.Column('reviewer_model', sa.String(length=100), nullable=True),
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(['artifact_id'], ['artifacts.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('artifact_id', 'artifact_version', name='uq_review_artifact_version')
+ )
+ with op.batch_alter_table('artifact_reviews', schema=None) as batch_op:
+ batch_op.create_index('ix_reviews_project', ['project_id'], unique=False)
+
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('artifact_reviews', schema=None) as batch_op:
+ batch_op.drop_index('ix_reviews_project')
+
+ op.drop_table('artifact_reviews')
+ # ### end Alembic commands ###
diff --git a/submissions/Victorious/apps/api/app/db/models.py b/submissions/Victorious/apps/api/app/db/models.py
new file mode 100644
index 00000000..5cc6e791
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/models.py
@@ -0,0 +1,287 @@
+"""SQLAlchemy table definitions.
+
+These mirror the domain models but are a separate layer on purpose. Domain models
+express engineering meaning and stay free of persistence; these express storage.
+Mapping between them happens in ``app/memory/sql_repository.py``.
+
+The duplication is deliberate and bounded: it is what lets the domain layer be
+framework-free (ADR-0003) and what allows the storage schema to be indexed,
+denormalised, or migrated without any agent noticing.
+"""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from typing import Any, ClassVar
+
+from sqlalchemy import (
+ JSON,
+ Boolean,
+ DateTime,
+ Float,
+ ForeignKey,
+ Index,
+ Integer,
+ String,
+ Text,
+ UniqueConstraint,
+)
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
+
+
+def _utcnow() -> datetime:
+ return datetime.now(UTC)
+
+
+class Base(DeclarativeBase):
+ """Declarative base.
+
+ ``JSON`` is used rather than PostgreSQL's ``JSONB`` so the same schema runs on
+ SQLite for native development and PostgreSQL in compose (ADR-0005). Where
+ Milestone 8 needs indexed JSON containment queries, a dialect-specific index
+ can be added in a migration without changing these definitions.
+ """
+
+ type_annotation_map: ClassVar[dict[Any, Any]] = {dict[str, Any]: JSON, list[str]: JSON}
+
+
+class ProjectRow(Base):
+ __tablename__ = "projects"
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ name: Mapped[str] = mapped_column(String(200), nullable=False)
+ description: Mapped[str] = mapped_column(Text, nullable=False)
+ current_stage: Mapped[str] = mapped_column(String(50), nullable=False)
+ stages: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
+
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+ updated_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
+ )
+
+ artifacts: Mapped[list[ArtifactRow]] = relationship(
+ back_populates="project", cascade="all, delete-orphan"
+ )
+
+
+class ArtifactRow(Base):
+ __tablename__ = "artifacts"
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ project_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
+ )
+ type: Mapped[str] = mapped_column(String(50), nullable=False)
+ title: Mapped[str] = mapped_column(String(300), nullable=False)
+ stage: Mapped[str] = mapped_column(String(50), nullable=False)
+ owner_role: Mapped[str] = mapped_column(String(50), nullable=False)
+ status: Mapped[str] = mapped_column(String(30), nullable=False)
+
+ current_version: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
+
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+ updated_at: Mapped[datetime] = mapped_column(
+ DateTime(timezone=True), default=_utcnow, onupdate=_utcnow
+ )
+
+ project: Mapped[ProjectRow] = relationship(back_populates="artifacts")
+ versions: Mapped[list[ArtifactVersionRow]] = relationship(
+ back_populates="artifact",
+ cascade="all, delete-orphan",
+ order_by="ArtifactVersionRow.version",
+ )
+
+ __table_args__ = (
+ # The Knowledge Base and every stage-scoped context read filter on these.
+ Index("ix_artifacts_project_stage", "project_id", "stage"),
+ Index("ix_artifacts_project_type", "project_id", "type"),
+ )
+
+
+class ArtifactVersionRow(Base):
+ __tablename__ = "artifact_versions"
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ artifact_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("artifacts.id", ondelete="CASCADE"), nullable=False
+ )
+ version: Mapped[int] = mapped_column(Integer, nullable=False)
+
+ body_markdown: Mapped[str] = mapped_column(Text, nullable=False)
+ content: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
+
+ produced_by_run_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
+ summary: Mapped[str] = mapped_column(Text, default="")
+ confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
+
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+
+ artifact: Mapped[ArtifactRow] = relationship(back_populates="versions")
+
+ __table_args__ = (
+ # Enforces append-only versioning at the storage layer: a duplicate
+ # version number is rejected by the database, not merely avoided by
+ # application code.
+ UniqueConstraint("artifact_id", "version", name="uq_artifact_version"),
+ )
+
+
+class ArtifactReviewRow(Base):
+ __tablename__ = "artifact_reviews"
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ project_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
+ )
+ artifact_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("artifacts.id", ondelete="CASCADE"), nullable=False
+ )
+ artifact_version: Mapped[int] = mapped_column(Integer, nullable=False)
+
+ stage: Mapped[str] = mapped_column(String(50), nullable=False)
+ role: Mapped[str] = mapped_column(String(50), nullable=False)
+ produced_by_run_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
+
+ quality_score: Mapped[int] = mapped_column(Integer, nullable=False)
+ verdict: Mapped[str] = mapped_column(String(40), nullable=False)
+ summary: Mapped[str] = mapped_column(Text, default="")
+
+ strengths: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
+ weaknesses: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
+ suggestions: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
+
+ deterministic_score: Mapped[int] = mapped_column(Integer, default=0)
+ reasoning_applied: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ reviewer_provider: Mapped[str | None] = mapped_column(String(50), nullable=True)
+ reviewer_model: Mapped[str | None] = mapped_column(String(100), nullable=True)
+
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+
+ __table_args__ = (
+ # One review per artifact version: a re-review supersedes rather than
+ # accumulates, and the version is what a score is attached to.
+ UniqueConstraint("artifact_id", "artifact_version", name="uq_review_artifact_version"),
+ Index("ix_reviews_project", "project_id"),
+ )
+
+
+class TraceEdgeRow(Base):
+ __tablename__ = "trace_edges"
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ project_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
+ )
+
+ upstream_artifact_id: Mapped[str] = mapped_column(String(64), nullable=False)
+ downstream_artifact_id: Mapped[str] = mapped_column(String(64), nullable=False)
+ kind: Mapped[str] = mapped_column(String(30), nullable=False)
+ upstream_version: Mapped[int] = mapped_column(Integer, nullable=False)
+
+ created_by_run_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
+ rationale: Mapped[str] = mapped_column(Text, default="")
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+
+ __table_args__ = (
+ # Impact analysis walks downstream; "why does this exist?" walks upstream.
+ # Both directions are indexed because both are hot paths.
+ Index("ix_trace_upstream", "upstream_artifact_id"),
+ Index("ix_trace_downstream", "downstream_artifact_id"),
+ Index("ix_trace_project", "project_id"),
+ )
+
+
+class AgentRunRow(Base):
+ __tablename__ = "agent_runs"
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ project_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
+ )
+ role: Mapped[str] = mapped_column(String(50), nullable=False)
+ stage: Mapped[str] = mapped_column(String(50), nullable=False)
+ status: Mapped[str] = mapped_column(String(30), nullable=False)
+
+ task: Mapped[str] = mapped_column(Text, default="")
+ reasoning_summary: Mapped[str] = mapped_column(Text, default="")
+ confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
+
+ input_artifact_ids: Mapped[list[str]] = mapped_column(JSON, default=list)
+ output_artifact_ids: Mapped[list[str]] = mapped_column(JSON, default=list)
+ blocked_on: Mapped[list[str]] = mapped_column(JSON, default=list)
+
+ provider: Mapped[str | None] = mapped_column(String(50), nullable=True)
+ model: Mapped[str | None] = mapped_column(String(100), nullable=True)
+ input_tokens: Mapped[int] = mapped_column(Integer, default=0)
+ output_tokens: Mapped[int] = mapped_column(Integer, default=0)
+
+ requires_approval: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
+ approval_reason: Mapped[str] = mapped_column(Text, default="")
+
+ correlation_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
+ error: Mapped[str | None] = mapped_column(Text, nullable=True)
+
+ started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+ completed_at: Mapped[datetime | None] = mapped_column(
+ DateTime(timezone=True), nullable=True
+ )
+
+ __table_args__ = (
+ Index("ix_runs_project_started", "project_id", "started_at"),
+ Index("ix_runs_project_role", "project_id", "role"),
+ )
+
+
+class ApprovalRow(Base):
+ __tablename__ = "approvals"
+
+ id: Mapped[str] = mapped_column(String(64), primary_key=True)
+ project_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
+ )
+ kind: Mapped[str] = mapped_column(String(50), nullable=False)
+ stage: Mapped[str] = mapped_column(String(50), nullable=False)
+
+ title: Mapped[str] = mapped_column(String(300), nullable=False)
+ what_changed: Mapped[str] = mapped_column(Text, nullable=False)
+ why: Mapped[str] = mapped_column(Text, nullable=False)
+
+ requested_by: Mapped[str] = mapped_column(String(50), nullable=False)
+ agents_involved: Mapped[list[str]] = mapped_column(JSON, default=list)
+ artifact_ids: Mapped[list[str]] = mapped_column(JSON, default=list)
+ impact: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
+
+ status: Mapped[str] = mapped_column(String(30), nullable=False)
+ feedback: Mapped[str | None] = mapped_column(Text, nullable=True)
+ decided_at: Mapped[datetime | None] = mapped_column(
+ DateTime(timezone=True), nullable=True
+ )
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+
+ __table_args__ = (Index("ix_approvals_project_status", "project_id", "status"),)
+
+
+class EventRow(Base):
+ __tablename__ = "events"
+
+ # Monotonic sequence, separate from the public ID: it gives the live stream a
+ # reliable resume cursor. Timestamps alone are not enough — two events can
+ # share a millisecond.
+ seq: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
+
+ id: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
+ project_id: Mapped[str] = mapped_column(
+ String(64), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
+ )
+ type: Mapped[str] = mapped_column(String(50), nullable=False)
+
+ stage: Mapped[str | None] = mapped_column(String(50), nullable=True)
+ role: Mapped[str | None] = mapped_column(String(50), nullable=True)
+
+ summary: Mapped[str] = mapped_column(Text, nullable=False)
+ payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
+
+ correlation_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
+
+ __table_args__ = (Index("ix_events_project_seq", "project_id", "seq"),)
diff --git a/submissions/Victorious/apps/api/app/db/session.py b/submissions/Victorious/apps/api/app/db/session.py
new file mode 100644
index 00000000..2e7f3237
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/db/session.py
@@ -0,0 +1,93 @@
+"""Database engine and session lifecycle."""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager
+
+from sqlalchemy.ext.asyncio import (
+ AsyncEngine,
+ AsyncSession,
+ async_sessionmaker,
+ create_async_engine,
+)
+
+from app.core.config import DatabaseSettings
+from app.core.logging import get_logger
+from app.db.models import Base
+
+logger = get_logger(__name__)
+
+
+class Database:
+ """Owns the engine and hands out sessions.
+
+ Registered as a container singleton so the engine — and its connection pool —
+ is created once per process and disposed on shutdown via ``aclose``.
+ """
+
+ def __init__(self, settings: DatabaseSettings) -> None:
+ self._settings = settings
+ self._engine: AsyncEngine = self._create_engine(settings)
+ self._session_factory = async_sessionmaker(
+ self._engine,
+ expire_on_commit=False,
+ class_=AsyncSession,
+ )
+
+ @staticmethod
+ def _create_engine(settings: DatabaseSettings) -> AsyncEngine:
+ """Build the engine, applying pooling only where it is meaningful.
+
+ SQLite rejects pool sizing arguments; passing them raises rather than
+ being ignored, so they are supplied only for server-backed dialects.
+ """
+ is_sqlite = settings.url.startswith("sqlite")
+
+ if is_sqlite:
+ return create_async_engine(settings.url, echo=settings.echo, future=True)
+
+ return create_async_engine(
+ settings.url,
+ echo=settings.echo,
+ future=True,
+ pool_size=settings.pool_size,
+ max_overflow=settings.max_overflow,
+ pool_pre_ping=True,
+ )
+
+ @property
+ def engine(self) -> AsyncEngine:
+ return self._engine
+
+ @asynccontextmanager
+ async def session(self) -> AsyncIterator[AsyncSession]:
+ """Yield a session inside a transaction.
+
+ Commits on clean exit, rolls back on any exception. Callers therefore
+ never manage transactions themselves, which is what makes a partially
+ written artifact impossible: either the version row, the artifact update,
+ and the trace edges all land, or none of them do.
+ """
+ async with self._session_factory() as session:
+ try:
+ yield session
+ await session.commit()
+ except Exception:
+ await session.rollback()
+ raise
+
+ async def create_schema(self) -> None:
+ """Create every table.
+
+ For tests and first-run local development. Alembic owns schema evolution
+ for anything long-lived — see ``app/db/migrations``.
+ """
+ async with self._engine.begin() as connection:
+ await connection.run_sync(Base.metadata.create_all)
+ logger.info("Database schema ensured")
+
+ async def aclose(self) -> None:
+ """Dispose the engine and its pool. Invoked by the container on shutdown."""
+ await self._engine.dispose()
+ logger.debug("Database engine disposed")
diff --git a/submissions/Victorious/apps/api/app/domain/__init__.py b/submissions/Victorious/apps/api/app/domain/__init__.py
new file mode 100644
index 00000000..f6267327
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/__init__.py
@@ -0,0 +1,109 @@
+"""Pure domain layer — the innermost ring of the architecture.
+
+Holds the vocabulary of the engineering organization: projects, lifecycle stages,
+artifacts and their versions, traceability edges, agent runs, approvals, events,
+and the errors that describe engineering failures.
+
+This package must remain free of frameworks, I/O, and persistence. Anything here
+can be exercised without a database, a network, or an event loop, which is what
+makes the orchestration rules testable in isolation.
+
+Enforced by ``tests/test_architecture.py``.
+"""
+
+from app.domain.agents import (
+ AgentMessage,
+ AgentRun,
+ AgentRunStatus,
+ TokenUsage,
+)
+from app.domain.approvals import ApprovalKind, ApprovalRequest, ApprovalStatus
+from app.domain.artifacts import (
+ Artifact,
+ ArtifactStatus,
+ ArtifactType,
+ ArtifactVersion,
+ ArtifactWithVersion,
+)
+from app.domain.errors import (
+ ApprovalRequiredError,
+ ConflictError,
+ DependencyNotSatisfiedError,
+ NotFoundError,
+ ProviderError,
+ ValidationError,
+ VictoriousError,
+)
+from app.domain.events import EventType, ProjectEvent
+from app.domain.ids import IdPrefix, is_id_of, new_id, prefix_of
+from app.domain.lifecycle import (
+ ROLE_TITLES,
+ STAGE_OWNERS,
+ STAGE_SEQUENCE,
+ AgentRole,
+ LifecycleStage,
+ StageStatus,
+ next_stage,
+ preceding_stages,
+ stage_index,
+)
+from app.domain.projects import Project, StageState
+from app.domain.traceability import (
+ ImpactAnalysis,
+ ImpactedArtifact,
+ StaleEdge,
+ TraceEdge,
+ TraceKind,
+ analyse_impact,
+ stale_artifact_ids,
+ stale_edges,
+ upstream_of,
+)
+
+__all__ = [
+ "ROLE_TITLES",
+ "STAGE_OWNERS",
+ "STAGE_SEQUENCE",
+ "AgentMessage",
+ "AgentRole",
+ "AgentRun",
+ "AgentRunStatus",
+ "ApprovalKind",
+ "ApprovalRequest",
+ "ApprovalRequiredError",
+ "ApprovalStatus",
+ "Artifact",
+ "ArtifactStatus",
+ "ArtifactType",
+ "ArtifactVersion",
+ "ArtifactWithVersion",
+ "ConflictError",
+ "DependencyNotSatisfiedError",
+ "EventType",
+ "IdPrefix",
+ "ImpactAnalysis",
+ "ImpactedArtifact",
+ "LifecycleStage",
+ "NotFoundError",
+ "Project",
+ "ProjectEvent",
+ "ProviderError",
+ "StageState",
+ "StageStatus",
+ "StaleEdge",
+ "TokenUsage",
+ "TraceEdge",
+ "TraceKind",
+ "ValidationError",
+ "VictoriousError",
+ "analyse_impact",
+ "is_id_of",
+ "new_id",
+ "next_stage",
+ "preceding_stages",
+ "prefix_of",
+ "stage_index",
+ "stale_artifact_ids",
+ "stale_edges",
+ "upstream_of",
+]
diff --git a/submissions/Victorious/apps/api/app/domain/agents.py b/submissions/Victorious/apps/api/app/domain/agents.py
new file mode 100644
index 00000000..0fd5c7b9
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/agents.py
@@ -0,0 +1,186 @@
+"""Agent runs and the structured message contract between agents.
+
+`05_AI_Agent_Architecture.md` specifies that "agents communicate using structured
+messages instead of free-form conversations", with every interaction carrying
+sender, receiver, task, context, dependencies, decision, confidence, and required
+actions. :class:`AgentMessage` is that contract, expressed as a type.
+
+:class:`AgentRun` is the persisted record of one agent doing one piece of work.
+It is what the Agent Organization view renders, and what
+`10_UI_UX_Plan.md` requires each agent to expose: current status, assigned
+responsibilities, current task, confidence level, dependencies, generated
+outputs, recent decisions.
+"""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from enum import StrEnum
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from app.domain.ids import IdPrefix, new_id
+from app.domain.lifecycle import AgentRole, LifecycleStage
+
+
+class AgentRunStatus(StrEnum):
+ """What an agent is doing right now.
+
+ These are exactly the states `07_System_Architecture.md` requires the
+ workspace to distinguish: "whether an agent is active, waiting for
+ dependencies, requesting approval, reviewing another agent's work, or idle".
+ """
+
+ QUEUED = "queued"
+ ACTIVE = "active"
+ WAITING_ON_DEPENDENCY = "waiting_on_dependency"
+ AWAITING_APPROVAL = "awaiting_approval"
+ REVIEWING = "reviewing"
+ COMPLETED = "completed"
+ FAILED = "failed"
+ CANCELLED = "cancelled"
+
+ @property
+ def is_terminal(self) -> bool:
+ """Whether no further transition is expected."""
+ return self in {
+ AgentRunStatus.COMPLETED,
+ AgentRunStatus.FAILED,
+ AgentRunStatus.CANCELLED,
+ }
+
+ @property
+ def is_running(self) -> bool:
+ """Whether the agent is occupying orchestration capacity."""
+ return self in {
+ AgentRunStatus.ACTIVE,
+ AgentRunStatus.REVIEWING,
+ }
+
+
+class AgentMessage(BaseModel):
+ """A structured communication between two agents.
+
+ Routed through the Executive AI rather than sent peer to peer, per
+ `05_AI_Agent_Architecture.md`: "Agents should avoid directly modifying each
+ other's internal state and instead exchange structured messages through the
+ Executive AI (Engineering Director)."
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ sender: AgentRole
+ receiver: AgentRole
+ task: str = Field(description="What the receiver is being asked to do.")
+
+ context_artifact_ids: list[str] = Field(
+ default_factory=list,
+ description="Artifacts the receiver should read. Resolved from shared memory.",
+ )
+ dependencies: list[str] = Field(
+ default_factory=list,
+ description="Artifact IDs that must exist and be approved before proceeding.",
+ )
+
+ decision: str | None = Field(
+ default=None, description="The decision being communicated, if any."
+ )
+ confidence: float | None = Field(default=None, ge=0.0, le=1.0)
+ required_actions: list[str] = Field(
+ default_factory=list, description="What the receiver must do in response."
+ )
+
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+
+
+class TokenUsage(BaseModel):
+ """Tokens consumed by one agent run.
+
+ Recorded because `12_Risk_Analysis.md` rates High Token Consumption a Medium
+ risk. Measuring it is the precondition for the caching decision deferred in
+ ADR-0005 — the intent is to decide from data rather than assumption.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ input_tokens: int = Field(default=0, ge=0)
+ output_tokens: int = Field(default=0, ge=0)
+
+ @property
+ def total(self) -> int:
+ return self.input_tokens + self.output_tokens
+
+
+class AgentRun(BaseModel):
+ """One agent performing one unit of engineering work.
+
+ The unit of observability for the whole platform: the Agent Organization view
+ renders live runs, the Engineering Timeline renders completed ones, and every
+ artifact version points back to the run that produced it.
+ """
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.AGENT_RUN))
+ project_id: str
+ role: AgentRole
+ stage: LifecycleStage
+
+ status: AgentRunStatus = AgentRunStatus.QUEUED
+ task: str = Field(default="", description="Human-readable current task.")
+
+ reasoning_summary: str = Field(
+ default="",
+ description=(
+ "Why the agent decided what it did, in prose. `12_Risk_Analysis.md` "
+ "names explainable reasoning as the mitigation for loss of user trust; "
+ "this is the field the workspace surfaces to earn it."
+ ),
+ )
+ confidence: float | None = Field(default=None, ge=0.0, le=1.0)
+
+ input_artifact_ids: list[str] = Field(
+ default_factory=list, description="Artifacts read as context."
+ )
+ output_artifact_ids: list[str] = Field(
+ default_factory=list, description="Artifacts written."
+ )
+ blocked_on: list[str] = Field(
+ default_factory=list,
+ description="Artifact IDs the run is waiting for, shown as its dependencies.",
+ )
+
+ provider: str | None = Field(default=None, description="LLM provider used.")
+ model: str | None = Field(default=None)
+ token_usage: TokenUsage = Field(default_factory=TokenUsage)
+
+ requires_approval: bool = Field(
+ default=False,
+ description=(
+ "Whether the agent judged its own output too consequential to proceed "
+ "on without a human. `09_MVP_Roadmap.md` requires approval of "
+ "technology selection and major engineering decisions, neither of "
+ "which is stage-shaped — they arise from what an agent concludes, so "
+ "the agent has to be able to raise the gate itself."
+ ),
+ )
+ approval_reason: str = Field(
+ default="", description="Why the agent asked for review."
+ )
+
+ correlation_id: str | None = Field(
+ default=None,
+ description=(
+ "Ties this run to the HTTP request that triggered it and to every log "
+ "line it emitted."
+ ),
+ )
+ error: str | None = Field(default=None, description="Failure detail, if failed.")
+
+ started_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+ completed_at: datetime | None = None
+
+ @property
+ def duration_seconds(self) -> float | None:
+ """Wall-clock duration, or ``None`` while still running."""
+ if self.completed_at is None:
+ return None
+ return (self.completed_at - self.started_at).total_seconds()
diff --git a/submissions/Victorious/apps/api/app/domain/approvals.py b/submissions/Victorious/apps/api/app/domain/approvals.py
new file mode 100644
index 00000000..c62da232
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/approvals.py
@@ -0,0 +1,107 @@
+"""Human approval gates.
+
+Required by five specification documents — `05`, `06`, `09`, `10`, and `12` —
+which makes this the least negotiable feature in the MVP.
+
+:class:`ApprovalRequest` carries exactly the five fields `10_UI_UX_Plan.md`
+requires the Approval Center to show: what changed, why it changed, which agents
+were involved, the downstream impact, and the available actions.
+"""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from enum import StrEnum
+
+from pydantic import BaseModel, Field
+
+from app.domain.ids import IdPrefix, new_id
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.traceability import ImpactAnalysis
+
+
+class ApprovalKind(StrEnum):
+ """What is being approved.
+
+ The set named in `09_MVP_Roadmap.md` ("Users must approve: Requirements,
+ Architecture, Technology Stack, Major Engineering Decisions, Final Code
+ Generation") plus requirement changes, which trigger the re-synchronisation
+ flow in Milestone 8.
+ """
+
+ REQUIREMENTS = "requirements"
+ ARCHITECTURE = "architecture"
+ TECHNOLOGY_SELECTION = "technology_selection"
+ ENGINEERING_DECISION = "engineering_decision"
+ CODE_GENERATION = "code_generation"
+ REQUIREMENT_CHANGE = "requirement_change"
+ RESYNCHRONISATION = "resynchronisation"
+
+
+class ApprovalStatus(StrEnum):
+ """Outcome of an approval request."""
+
+ PENDING = "pending"
+ APPROVED = "approved"
+ REJECTED = "rejected"
+ CHANGES_REQUESTED = "changes_requested"
+
+ @property
+ def is_decided(self) -> bool:
+ return self is not ApprovalStatus.PENDING
+
+ @property
+ def unblocks_progress(self) -> bool:
+ """Whether the orchestrator may proceed past this gate."""
+ return self is ApprovalStatus.APPROVED
+
+
+class ApprovalRequest(BaseModel):
+ """A decision suspended pending human review.
+
+ While one of these is pending, the orchestration graph is genuinely halted —
+ no downstream artifact is written. `12_Risk_Analysis.md` lists Excessive
+ Automation as a High risk mitigated by "human approval checkpoints"; a gate
+ that merely notified the user while work continued would not be one.
+ """
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.APPROVAL))
+ project_id: str
+ kind: ApprovalKind
+ stage: LifecycleStage
+
+ title: str
+ what_changed: str = Field(description="Plain-language description of the change.")
+ why: str = Field(description="Reasoning that produced it.")
+
+ requested_by: AgentRole
+ agents_involved: list[AgentRole] = Field(
+ default_factory=list, description="Every role that contributed."
+ )
+
+ artifact_ids: list[str] = Field(
+ default_factory=list, description="Artifacts under review."
+ )
+ impact: ImpactAnalysis | None = Field(
+ default=None,
+ description=(
+ "Downstream blast radius, computed before the decision so the reviewer "
+ "sees the consequences of approving rather than discovering them after."
+ ),
+ )
+
+ status: ApprovalStatus = ApprovalStatus.PENDING
+ feedback: str | None = Field(
+ default=None,
+ description=(
+ "Reviewer's note on rejection or change request. Fed back into the "
+ "agent's context on re-run, so a rejection teaches rather than repeats."
+ ),
+ )
+ decided_at: datetime | None = None
+
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+
+ @property
+ def is_pending(self) -> bool:
+ return self.status is ApprovalStatus.PENDING
diff --git a/submissions/Victorious/apps/api/app/domain/artifacts.py b/submissions/Victorious/apps/api/app/domain/artifacts.py
new file mode 100644
index 00000000..fcf7b3e8
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/artifacts.py
@@ -0,0 +1,172 @@
+"""Engineering artifacts and their append-only version history.
+
+The model separates two things that are usually conflated:
+
+- **Artifact** — stable identity. "The system architecture for project X." Its ID
+ never changes, so traceability edges pointing at it survive every revision.
+- **ArtifactVersion** — immutable content at a point in time. Never updated, only
+ superseded.
+
+That separation is what makes `12_Risk_Analysis.md`'s "version-controlled
+engineering artifacts" mitigation real: revising an artifact appends a version
+and leaves every prior version intact and citable, so a decision made in week one
+can still be inspected exactly as the agent that consumed it saw it.
+"""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from enum import StrEnum
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from app.domain.ids import IdPrefix, new_id
+from app.domain.lifecycle import AgentRole, LifecycleStage
+
+
+class ArtifactType(StrEnum):
+ """Every artifact the MVP organization produces.
+
+ Drawn from the Engineering Artifacts list in `09_MVP_Roadmap.md` and the
+ per-agent outputs in `05_AI_Agent_Architecture.md`.
+ """
+
+ # Product Manager
+ PRD = "prd"
+ USER_STORIES = "user_stories"
+ FUNCTIONAL_REQUIREMENTS = "functional_requirements"
+ NON_FUNCTIONAL_REQUIREMENTS = "non_functional_requirements"
+ ACCEPTANCE_CRITERIA = "acceptance_criteria"
+
+ # Business Analyst
+ BUSINESS_ANALYSIS = "business_analysis"
+ GAP_ANALYSIS = "gap_analysis"
+ RISK_REGISTER = "risk_register"
+
+ # Software Architect
+ SYSTEM_ARCHITECTURE = "system_architecture"
+ API_CONTRACT = "api_contract"
+ DATABASE_SCHEMA = "database_schema"
+ TECHNOLOGY_DECISION = "technology_decision"
+ ENGINEERING_DECISION = "engineering_decision"
+ IMPLEMENTATION_PLAN = "implementation_plan"
+
+ # Full Stack Engineer
+ REPOSITORY_STRUCTURE = "repository_structure"
+ SOURCE_FILE = "source_file"
+
+ # QA Engineer
+ TEST_PLAN = "test_plan"
+ TEST_CASES = "test_cases"
+ COVERAGE_REPORT = "coverage_report"
+
+ # Documentation
+ README = "readme"
+ API_DOCUMENTATION = "api_documentation"
+ ARCHITECTURE_DOCUMENT = "architecture_document"
+ DEVELOPER_GUIDE = "developer_guide"
+ CHANGELOG = "changelog"
+ DEPLOYMENT_PLAN = "deployment_plan"
+
+
+class ArtifactStatus(StrEnum):
+ """Approval state of an artifact.
+
+ Deliberately excludes staleness. Whether an artifact has fallen behind its
+ upstream is *derived* from the traceability graph
+ (:func:`app.domain.traceability.stale_edges`), never stored — a stored flag
+ would be one more thing that can silently disagree with reality, which is the
+ exact failure this platform exists to prevent.
+ """
+
+ DRAFT = "draft"
+ AWAITING_APPROVAL = "awaiting_approval"
+ APPROVED = "approved"
+ REJECTED = "rejected"
+
+
+class ArtifactVersion(BaseModel):
+ """Immutable content of an artifact at one point in time."""
+
+ model_config = ConfigDict(frozen=True)
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.VERSION))
+ artifact_id: str
+ version: int = Field(ge=1, description="1-based, contiguous, never reused.")
+
+ body_markdown: str = Field(
+ description="Rendered form, shown in the workspace and the Knowledge Base."
+ )
+ content: dict[str, Any] = Field(
+ default_factory=dict,
+ description=(
+ "Structured form, validated against the producing agent's output "
+ "contract. Downstream agents read this rather than parsing prose."
+ ),
+ )
+
+ produced_by_run_id: str | None = Field(
+ default=None,
+ description=(
+ "The agent run that authored this version. Half of the traceability "
+ "guarantee: every artifact answers 'which agent produced me, and why'."
+ ),
+ )
+ summary: str = Field(
+ default="",
+ description="One line on what changed and why, shown in version history.",
+ )
+ confidence: float | None = Field(
+ default=None,
+ ge=0.0,
+ le=1.0,
+ description=(
+ "Producing agent's self-reported confidence. `12_Risk_Analysis.md` "
+ "lists confidence scoring as a hallucination mitigation."
+ ),
+ )
+
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+
+
+class Artifact(BaseModel):
+ """Stable identity of an engineering artifact across all its versions."""
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.ARTIFACT))
+ project_id: str
+ type: ArtifactType
+ title: str
+
+ stage: LifecycleStage = Field(description="Lifecycle stage that produced it.")
+ owner_role: AgentRole = Field(description="Agent role responsible for it.")
+
+ status: ArtifactStatus = ArtifactStatus.DRAFT
+ current_version: int = Field(default=0, ge=0, description="0 until first write.")
+
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+
+ @property
+ def has_content(self) -> bool:
+ """Whether any version has been written yet."""
+ return self.current_version > 0
+
+ @property
+ def is_approved(self) -> bool:
+ return self.status is ArtifactStatus.APPROVED
+
+
+class ArtifactWithVersion(BaseModel):
+ """An artifact together with one of its versions.
+
+ The shape the API and agents actually want: identity plus content, resolved
+ in a single read rather than two.
+ """
+
+ artifact: Artifact
+ version: ArtifactVersion
+
+ @property
+ def is_latest(self) -> bool:
+ return self.version.version == self.artifact.current_version
diff --git a/submissions/Victorious/apps/api/app/domain/errors.py b/submissions/Victorious/apps/api/app/domain/errors.py
new file mode 100644
index 00000000..7261e58f
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/errors.py
@@ -0,0 +1,94 @@
+"""Domain-level error hierarchy.
+
+This module is deliberately free of any framework, transport, or persistence
+concern. Domain code raises these errors to describe *what went wrong in the
+engineering domain*; the transport layer (``app.core.errors``) is solely
+responsible for deciding how each one is represented over HTTP.
+
+Keeping the two separate is what allows the orchestration and agent layers to be
+exercised in tests, in a CLI, or inside a background worker without dragging
+FastAPI into the domain.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+
+class VictoriousError(Exception):
+ """Base class for every error raised by Project Victorious domain code.
+
+ Attributes:
+ message: Human-readable description, safe to surface to an operator.
+ code: Stable, machine-readable identifier. Clients switch on this rather
+ than on message text, so it must not change once released.
+ details: Structured context for debugging and for the Approval Center to
+ render *why* something failed. Must never contain secrets.
+ """
+
+ code: str = "victorious_error"
+
+ def __init__(self, message: str, *, details: dict[str, Any] | None = None) -> None:
+ super().__init__(message)
+ self.message = message
+ self.details: dict[str, Any] = details or {}
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}(code={self.code!r}, message={self.message!r})"
+
+
+class NotFoundError(VictoriousError):
+ """A requested entity does not exist in the shared organizational memory."""
+
+ code = "not_found"
+
+
+class ValidationError(VictoriousError):
+ """Input violated a domain invariant.
+
+ Distinct from a request-schema failure, which never reaches the domain.
+ """
+
+ code = "validation_error"
+
+
+class ConflictError(VictoriousError):
+ """The requested change conflicts with the current state of the project.
+
+ Raised, for example, when two engineering agents produce contradictory
+ artifacts for the same lifecycle stage, or when an artifact version is
+ superseded concurrently.
+ """
+
+ code = "conflict"
+
+
+class DependencyNotSatisfiedError(VictoriousError):
+ """An engineering stage or agent was invoked before its inputs were ready.
+
+ The Executive AI relies on this to enforce lifecycle ordering rather than
+ letting an agent reason over incomplete upstream context.
+ """
+
+ code = "dependency_not_satisfied"
+
+
+class ApprovalRequiredError(VictoriousError):
+ """A human approval gate blocks the requested transition.
+
+ Surfaces the human-in-the-loop guarantee as a first-class domain outcome
+ instead of an implicit branch inside the orchestrator.
+ """
+
+ code = "approval_required"
+
+
+class ProviderError(VictoriousError):
+ """An external provider (LLM, vector store, cache) failed.
+
+ Wrapping provider faults in a domain error keeps retry, fallback, and
+ degradation policy decisions inside the platform rather than leaking a
+ vendor SDK exception into orchestration code.
+ """
+
+ code = "provider_error"
diff --git a/submissions/Victorious/apps/api/app/domain/events.py b/submissions/Victorious/apps/api/app/domain/events.py
new file mode 100644
index 00000000..15e09b60
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/events.py
@@ -0,0 +1,84 @@
+"""Project events — the engineering activity record.
+
+Every meaningful change is recorded as an event. Events serve three consumers
+from one write:
+
+- the **Engineering Timeline**, which `10_UI_UX_Plan.md` requires to preserve
+ completed stages so engineering history stays visible;
+- the **live agent stream** in Milestone 6, which pushes these to the browser;
+- the **audit trail**, since events are append-only and never revised.
+
+Events describe what happened. They are not the source of truth for current
+state — that is the artifact model. Deriving state by replaying events would be
+event sourcing, which this is deliberately not: it adds reconstruction complexity
+for a benefit the artifact version history already provides.
+"""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from enum import StrEnum
+from typing import Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from app.domain.ids import IdPrefix, new_id
+from app.domain.lifecycle import AgentRole, LifecycleStage
+
+
+class EventType(StrEnum):
+ """What happened.
+
+ Covers the notification triggers listed in `06_Product_Architecture.md`:
+ agent completion, approval requests, architecture conflicts, requirement
+ changes, test failures, documentation updates, dependency conflicts.
+ """
+
+ PROJECT_CREATED = "project_created"
+
+ STAGE_STARTED = "stage_started"
+ STAGE_COMPLETED = "stage_completed"
+ STAGE_BLOCKED = "stage_blocked"
+
+ AGENT_STARTED = "agent_started"
+ AGENT_PROGRESS = "agent_progress"
+ AGENT_COMPLETED = "agent_completed"
+ AGENT_FAILED = "agent_failed"
+
+ ARTIFACT_CREATED = "artifact_created"
+ ARTIFACT_REVISED = "artifact_revised"
+ ARTIFACT_APPROVED = "artifact_approved"
+ ARTIFACT_MARKED_STALE = "artifact_marked_stale"
+ ARTIFACT_REVIEWED = "artifact_reviewed"
+
+ APPROVAL_REQUESTED = "approval_requested"
+ APPROVAL_GRANTED = "approval_granted"
+ APPROVAL_REJECTED = "approval_rejected"
+ CHANGES_REQUESTED = "changes_requested"
+
+ CONFLICT_DETECTED = "conflict_detected"
+ IMPACT_ANALYSED = "impact_analysed"
+
+
+class ProjectEvent(BaseModel):
+ """One recorded occurrence in a project's engineering history."""
+
+ model_config = ConfigDict(frozen=True)
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.EVENT))
+ project_id: str
+ type: EventType
+
+ stage: LifecycleStage | None = None
+ role: AgentRole | None = None
+
+ summary: str = Field(description="One line, rendered directly in the timeline.")
+ payload: dict[str, Any] = Field(
+ default_factory=dict,
+ description="Type-specific detail: artifact IDs, versions, impact counts.",
+ )
+
+ correlation_id: str | None = Field(
+ default=None, description="Ties the event to the request and logs that produced it."
+ )
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
diff --git a/submissions/Victorious/apps/api/app/domain/ids.py b/submissions/Victorious/apps/api/app/domain/ids.py
new file mode 100644
index 00000000..f1898e85
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/ids.py
@@ -0,0 +1,48 @@
+"""Prefixed entity identifiers.
+
+Identifiers carry a type prefix (``art_9f2c…``, ``run_41ab…``). Two reasons:
+
+1. A traceability graph rendered in the UI, a log line, and an API response all
+ become self-describing — you can tell an artifact from an agent run without
+ consulting a schema.
+2. Passing an agent-run ID where an artifact ID belongs is caught by inspection
+ rather than by a confusing empty result.
+
+Random rather than sequential: identifiers appear in URLs, and sequential IDs
+would leak how many projects exist.
+"""
+
+from __future__ import annotations
+
+import uuid
+from enum import StrEnum
+
+
+class IdPrefix(StrEnum):
+ """Type prefixes for every persisted entity."""
+
+ PROJECT = "prj"
+ ARTIFACT = "art"
+ VERSION = "ver"
+ AGENT_RUN = "run"
+ TRACE_EDGE = "edg"
+ APPROVAL = "apr"
+ EVENT = "evt"
+ REVIEW = "rev"
+ TASK = "tsk"
+
+
+def new_id(prefix: IdPrefix) -> str:
+ """Mint a new identifier for the given entity type."""
+ return f"{prefix.value}_{uuid.uuid4().hex}"
+
+
+def prefix_of(identifier: str) -> str | None:
+ """Return the type prefix of an identifier, or ``None`` if malformed."""
+ head, separator, _ = identifier.partition("_")
+ return head if separator else None
+
+
+def is_id_of(identifier: str, prefix: IdPrefix) -> bool:
+ """Return whether ``identifier`` denotes an entity of the given type."""
+ return prefix_of(identifier) == prefix.value
diff --git a/submissions/Victorious/apps/api/app/domain/lifecycle.py b/submissions/Victorious/apps/api/app/domain/lifecycle.py
new file mode 100644
index 00000000..65bdbd1f
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/lifecycle.py
@@ -0,0 +1,109 @@
+"""Engineering lifecycle stages and the agent roles that own them.
+
+The stage sequence is taken verbatim from the Project Lifecycle in
+`09_MVP_Roadmap.md`. Encoding it as an ordered enum rather than free-form strings
+means the orchestrator can answer "what comes next" and "is this stage ready"
+from the model instead of from branching logic scattered across agents.
+"""
+
+from __future__ import annotations
+
+from enum import StrEnum
+
+
+class LifecycleStage(StrEnum):
+ """The nine engineering stages a project moves through.
+
+ Declaration order is execution order; ``STAGE_SEQUENCE`` below depends on it.
+ """
+
+ IDEA = "idea"
+ REQUIREMENT_DISCOVERY = "requirement_discovery"
+ BUSINESS_VALIDATION = "business_validation"
+ ARCHITECTURE = "architecture"
+ DEVELOPMENT_PLANNING = "development_planning"
+ IMPLEMENTATION = "implementation"
+ TESTING = "testing"
+ DOCUMENTATION = "documentation"
+ DEPLOYMENT_PREPARATION = "deployment_preparation"
+
+
+STAGE_SEQUENCE: tuple[LifecycleStage, ...] = tuple(LifecycleStage)
+
+
+class AgentRole(StrEnum):
+ """The MVP engineering organization.
+
+ Exactly the roster in `09_MVP_Roadmap.md`, `11_Future_Roadmap.md`, and
+ `14_Executive_Summary.md`. The Full Stack Engineer stands in for the separate
+ Frontend, Backend, and Database agents of `05_AI_Agent_Architecture.md` — an
+ explicit MVP simplification, not an architectural one; each becomes its own
+ role in V2 by adding members here and prompts in ``app/agents/prompts``.
+ """
+
+ EXECUTIVE = "executive"
+ PRODUCT_MANAGER = "product_manager"
+ BUSINESS_ANALYST = "business_analyst"
+ SOFTWARE_ARCHITECT = "software_architect"
+ FULL_STACK_ENGINEER = "full_stack_engineer"
+ QA_ENGINEER = "qa_engineer"
+ DOCUMENTATION = "documentation"
+
+
+#: Human-facing titles. Held here so the API, the Agent Organization view, and
+#: generated documentation all name a role identically.
+ROLE_TITLES: dict[AgentRole, str] = {
+ AgentRole.EXECUTIVE: "Executive AI (Engineering Director)",
+ AgentRole.PRODUCT_MANAGER: "Product Manager",
+ AgentRole.BUSINESS_ANALYST: "Business Analyst",
+ AgentRole.SOFTWARE_ARCHITECT: "Software Architect",
+ AgentRole.FULL_STACK_ENGINEER: "Full Stack Engineer",
+ AgentRole.QA_ENGINEER: "QA Engineer",
+ AgentRole.DOCUMENTATION: "Documentation Engineer",
+}
+
+#: Which role performs the engineering work of each stage.
+#:
+#: The Executive AI is deliberately absent: `15_Development_Guidelines.md` states
+#: it "coordinates engineering activities but does not directly perform
+#: engineering work". Its absence here is what keeps that true structurally.
+STAGE_OWNERS: dict[LifecycleStage, AgentRole] = {
+ LifecycleStage.REQUIREMENT_DISCOVERY: AgentRole.PRODUCT_MANAGER,
+ LifecycleStage.BUSINESS_VALIDATION: AgentRole.BUSINESS_ANALYST,
+ LifecycleStage.ARCHITECTURE: AgentRole.SOFTWARE_ARCHITECT,
+ LifecycleStage.DEVELOPMENT_PLANNING: AgentRole.SOFTWARE_ARCHITECT,
+ LifecycleStage.IMPLEMENTATION: AgentRole.FULL_STACK_ENGINEER,
+ LifecycleStage.TESTING: AgentRole.QA_ENGINEER,
+ LifecycleStage.DOCUMENTATION: AgentRole.DOCUMENTATION,
+ LifecycleStage.DEPLOYMENT_PREPARATION: AgentRole.DOCUMENTATION,
+}
+
+
+class StageStatus(StrEnum):
+ """Progress of one stage within a project."""
+
+ PENDING = "pending"
+ IN_PROGRESS = "in_progress"
+ AWAITING_APPROVAL = "awaiting_approval"
+ COMPLETED = "completed"
+ BLOCKED = "blocked"
+
+
+def stage_index(stage: LifecycleStage) -> int:
+ """Return the ordinal position of a stage in the lifecycle."""
+ return STAGE_SEQUENCE.index(stage)
+
+
+def next_stage(stage: LifecycleStage) -> LifecycleStage | None:
+ """Return the stage following ``stage``, or ``None`` at the end."""
+ index = stage_index(stage) + 1
+ return STAGE_SEQUENCE[index] if index < len(STAGE_SEQUENCE) else None
+
+
+def preceding_stages(stage: LifecycleStage) -> tuple[LifecycleStage, ...]:
+ """Return every stage that must complete before ``stage`` may run.
+
+ The orchestrator uses this to refuse an agent invocation whose upstream
+ context is incomplete, rather than letting it reason over a partial project.
+ """
+ return STAGE_SEQUENCE[: stage_index(stage)]
diff --git a/submissions/Victorious/apps/api/app/domain/projects.py b/submissions/Victorious/apps/api/app/domain/projects.py
new file mode 100644
index 00000000..00f95174
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/projects.py
@@ -0,0 +1,63 @@
+"""Projects and their lifecycle state."""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+
+from pydantic import BaseModel, Field
+
+from app.domain.ids import IdPrefix, new_id
+from app.domain.lifecycle import LifecycleStage, StageStatus
+
+
+class StageState(BaseModel):
+ """Progress of one lifecycle stage within a project.
+
+ Mutable rather than frozen: a stage genuinely moves backwards when a reviewer
+ rejects its output, and the organization reruns it.
+ """
+
+ stage: LifecycleStage
+ status: StageStatus = StageStatus.PENDING
+ started_at: datetime | None = None
+ completed_at: datetime | None = None
+
+ @property
+ def is_complete(self) -> bool:
+ return self.status is StageStatus.COMPLETED
+
+
+class Project(BaseModel):
+ """A software engineering project.
+
+ Creation asks only for a name and a description. `07_System_Architecture.md`
+ is explicit: "Every project begins with minimal onboarding by asking only for
+ a project name and a brief description... Rather than forcing users through a
+ predefined interview before project creation, the platform should gradually
+ collect engineering knowledge while continuously updating project artifacts."
+
+ Everything else on this model is produced by the organization, not asked of
+ the user.
+ """
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.PROJECT))
+ name: str = Field(min_length=1, max_length=200)
+ description: str = Field(
+ min_length=1,
+ max_length=4000,
+ description="The idea, in the user's own words. The only required input.",
+ )
+
+ current_stage: LifecycleStage = LifecycleStage.IDEA
+ stages: list[StageState] = Field(default_factory=list)
+
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+
+ def stage_state(self, stage: LifecycleStage) -> StageState | None:
+ """Return the recorded state of a stage, if it has one."""
+ return next((state for state in self.stages if state.stage is stage), None)
+
+ @property
+ def completed_stages(self) -> list[LifecycleStage]:
+ return [state.stage for state in self.stages if state.is_complete]
diff --git a/submissions/Victorious/apps/api/app/domain/reviews.py b/submissions/Victorious/apps/api/app/domain/reviews.py
new file mode 100644
index 00000000..b07886ae
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/reviews.py
@@ -0,0 +1,119 @@
+"""Engineering review of a produced artifact.
+
+The organization checks its own work. After a specialist produces an artifact, a
+reviewer scores it and records what is strong, what is weak, and what would
+improve it — the engineering-review step a real organization performs before the
+work reaches a human.
+
+`12_Risk_Analysis.md` prescribes "cross-validation between engineering agents" as
+a mitigation for AI hallucination. The Business Analyst already validates the
+Product Manager's requirements; this generalises that check to every artifact,
+independent of the specialist that produced it.
+
+A review is attached to an artifact **version**, not to an artifact. Revising an
+artifact produces a new version that has not been reviewed, exactly as it has not
+been approved — a score must never outlive the content it was given.
+"""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from enum import StrEnum
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from app.domain.ids import IdPrefix, new_id
+from app.domain.lifecycle import AgentRole, LifecycleStage
+
+
+class ReviewVerdict(StrEnum):
+ """The reviewer's overall judgement."""
+
+ APPROVED = "approved"
+ """Sound as produced."""
+
+ APPROVED_WITH_SUGGESTIONS = "approved_with_suggestions"
+ """Usable, with improvements worth making."""
+
+ NEEDS_REVISION = "needs_revision"
+ """A defect a downstream specialist would inherit."""
+
+ @property
+ def is_acceptable(self) -> bool:
+ """Whether downstream work may proceed on this artifact."""
+ return self is not ReviewVerdict.NEEDS_REVISION
+
+
+class ReviewFinding(BaseModel):
+ """One observation, with the check that produced it.
+
+ ``source`` distinguishes a deterministic structural check from a reasoned
+ judgement. A user reading a weakness deserves to know whether it is a fact
+ about the artifact or an opinion about it.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ text: str
+ source: str = Field(
+ default="check",
+ description="'check' for a deterministic rule, 'reasoning' for a model judgement.",
+ )
+
+
+class ArtifactReview(BaseModel):
+ """A scored review of one version of one artifact."""
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.REVIEW))
+ project_id: str
+ artifact_id: str
+ artifact_version: int = Field(ge=1)
+
+ stage: LifecycleStage
+ role: AgentRole = Field(description="The specialist whose work is under review.")
+ produced_by_run_id: str | None = None
+
+ quality_score: int = Field(
+ ge=0,
+ le=100,
+ description=(
+ "Composite score. The deterministic checks set the floor and carry the "
+ "weight, so a score means something even with no model available; "
+ "reasoning adjusts it within a bounded range."
+ ),
+ )
+ verdict: ReviewVerdict = ReviewVerdict.APPROVED
+
+ summary: str = Field(default="", description="One line, shown in lists.")
+ strengths: list[ReviewFinding] = Field(default_factory=list)
+ weaknesses: list[ReviewFinding] = Field(default_factory=list)
+ suggestions: list[ReviewFinding] = Field(default_factory=list)
+
+ deterministic_score: int = Field(
+ default=0,
+ ge=0,
+ le=100,
+ description="The structural score before any reasoning adjustment.",
+ )
+ reasoning_applied: bool = Field(
+ default=False,
+ description=(
+ "Whether a model contributed. False means the review is purely "
+ "structural — honest, and visibly so in the workspace."
+ ),
+ )
+ reviewer_provider: str | None = None
+ reviewer_model: str | None = None
+
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+
+ @property
+ def band(self) -> str:
+ """Coarse quality band, for badges and grouping."""
+ if self.quality_score >= 85:
+ return "strong"
+ if self.quality_score >= 70:
+ return "sound"
+ if self.quality_score >= 50:
+ return "weak"
+ return "poor"
diff --git a/submissions/Victorious/apps/api/app/domain/traceability.py b/submissions/Victorious/apps/api/app/domain/traceability.py
new file mode 100644
index 00000000..4bf051fe
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/domain/traceability.py
@@ -0,0 +1,286 @@
+"""Traceability edges, staleness, and change impact.
+
+`04_Existing_Solutions.md` identifies the gap no existing tool fills:
+
+ Is the architecture still consistent with the latest requirements?
+ Which downstream components are affected by this requirement change?
+
+This module is the answer, and it is the reason the whole platform is more than a
+code generator. Two design choices carry it.
+
+**Edges bind artifact identity, but record upstream version.**
+An edge points from an upstream artifact to a downstream one and remembers *which
+version of the upstream* the downstream was derived from. Because artifact IDs
+are stable across revisions, the graph survives every edit; because the version
+is recorded, the graph knows when a derivation has gone out of date.
+
+**Staleness is computed, never stored.**
+An artifact is stale when an edge cites an upstream version older than that
+upstream's current version. There is no flag to set, so there is no flag to
+forget to set — the property this platform claims about engineering artifacts is
+one it structurally cannot violate itself.
+"""
+
+from __future__ import annotations
+
+from collections import deque
+from collections.abc import Iterable, Mapping
+from datetime import UTC, datetime
+from enum import StrEnum
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from app.domain.ids import IdPrefix, new_id
+
+
+class TraceKind(StrEnum):
+ """The engineering relationship an edge represents.
+
+ Kinds are not interchangeable: an architecture that *derives from* a
+ requirement goes stale when the requirement changes, whereas a business
+ analysis that *validates* one only needs re-examining. Milestone 8 uses this
+ to propose proportionate re-synchronisation instead of regenerating
+ everything downstream.
+ """
+
+ DERIVES_FROM = "derives_from"
+ """Downstream content was produced from upstream content."""
+
+ IMPLEMENTS = "implements"
+ """Downstream realises an upstream specification (code implements a design)."""
+
+ VALIDATES = "validates"
+ """Downstream checks upstream (business analysis validates requirements)."""
+
+ TESTS = "tests"
+ """Downstream verifies upstream (test cases test acceptance criteria)."""
+
+ DOCUMENTS = "documents"
+ """Downstream describes upstream (API docs document an API contract)."""
+
+ REFINES = "refines"
+ """Downstream adds detail without changing intent."""
+
+
+class TraceEdge(BaseModel):
+ """A directed dependency between two artifacts.
+
+ Immutable. A changed derivation is a new edge, so the history of how the
+ project was reasoned about is preserved alongside the artifacts themselves.
+ """
+
+ model_config = ConfigDict(frozen=True)
+
+ id: str = Field(default_factory=lambda: new_id(IdPrefix.TRACE_EDGE))
+ project_id: str
+
+ upstream_artifact_id: str = Field(description="The artifact depended upon.")
+ downstream_artifact_id: str = Field(description="The artifact that depends.")
+ kind: TraceKind = TraceKind.DERIVES_FROM
+
+ upstream_version: int = Field(
+ ge=1,
+ description=(
+ "Version of the upstream artifact this derivation consumed. The field "
+ "that makes staleness computable rather than declared."
+ ),
+ )
+
+ created_by_run_id: str | None = Field(
+ default=None, description="Agent run that established the dependency."
+ )
+ rationale: str = Field(
+ default="",
+ description="Why the dependency exists, surfaced in the impact preview.",
+ )
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
+
+
+class StaleEdge(BaseModel):
+ """An edge whose upstream has advanced past the version it cites."""
+
+ edge: TraceEdge
+ current_upstream_version: int
+
+ @property
+ def versions_behind(self) -> int:
+ return self.current_upstream_version - self.edge.upstream_version
+
+
+class ImpactedArtifact(BaseModel):
+ """One artifact inside a change's blast radius."""
+
+ artifact_id: str
+ depth: int = Field(
+ ge=1, description="Edge hops from the changed artifact. 1 is direct."
+ )
+ via_kind: TraceKind = Field(description="Relationship on the path's final hop.")
+ path: list[str] = Field(
+ description="Artifact IDs from the changed artifact to this one, inclusive."
+ )
+
+
+class ImpactAnalysis(BaseModel):
+ """Everything a change to one artifact would affect.
+
+ Computed and shown to the user *before* anything is regenerated — the
+ "downstream impact" field the Approval Center requires in
+ `10_UI_UX_Plan.md`.
+ """
+
+ changed_artifact_id: str
+ impacted: list[ImpactedArtifact] = Field(default_factory=list)
+
+ @property
+ def artifact_ids(self) -> list[str]:
+ return [item.artifact_id for item in self.impacted]
+
+ @property
+ def direct(self) -> list[ImpactedArtifact]:
+ """Artifacts one hop downstream — those that change most certainly."""
+ return [item for item in self.impacted if item.depth == 1]
+
+ @property
+ def is_empty(self) -> bool:
+ return not self.impacted
+
+
+def current_edges(edges: Iterable[TraceEdge]) -> list[TraceEdge]:
+ """Return only the most recent declaration of each dependency.
+
+ Edges are immutable, so an agent that reruns declares a *new* edge rather
+ than updating the old one — which means the graph accumulates a history of
+ derivations for the same pair. The earlier ones are exactly that: history.
+
+ Without this filter an artifact could never stop being stale. Rebuilding it
+ against the current upstream adds a fresh edge, but the superseded edge still
+ cites the old version, so the artifact would be reported out of date forever
+ no matter how many times it was regenerated.
+
+ Recency is judged by creation time, falling back to the cited upstream
+ version when two edges share a timestamp.
+ """
+ latest: dict[tuple[str, str, TraceKind], TraceEdge] = {}
+
+ for edge in edges:
+ key = (edge.upstream_artifact_id, edge.downstream_artifact_id, edge.kind)
+ existing = latest.get(key)
+ if existing is None or (edge.created_at, edge.upstream_version) >= (
+ existing.created_at,
+ existing.upstream_version,
+ ):
+ latest[key] = edge
+
+ return list(latest.values())
+
+
+def stale_edges(
+ edges: Iterable[TraceEdge],
+ current_versions: Mapping[str, int],
+) -> list[StaleEdge]:
+ """Return derivations whose upstream has advanced past the cited version.
+
+ Only the current declaration of each dependency is considered; superseded
+ edges are history (see :func:`current_edges`).
+
+ Args:
+ edges: Edges to examine.
+ current_versions: Artifact ID to its current version number.
+
+ Returns:
+ One entry per out-of-date dependency. An edge whose upstream is missing
+ from ``current_versions`` is skipped rather than assumed stale — an
+ unknown artifact is a caller bug, and guessing would produce false alarms
+ in the one place the platform must be trustworthy.
+ """
+ results: list[StaleEdge] = []
+
+ for edge in current_edges(edges):
+ current = current_versions.get(edge.upstream_artifact_id)
+ if current is None:
+ continue
+ if current > edge.upstream_version:
+ results.append(StaleEdge(edge=edge, current_upstream_version=current))
+
+ return results
+
+
+def stale_artifact_ids(
+ edges: Iterable[TraceEdge],
+ current_versions: Mapping[str, int],
+) -> set[str]:
+ """Return the artifacts that are out of date with respect to their upstream."""
+ return {stale.edge.downstream_artifact_id for stale in stale_edges(edges, current_versions)}
+
+
+def analyse_impact(
+ changed_artifact_id: str,
+ edges: Iterable[TraceEdge],
+ *,
+ max_depth: int | None = None,
+) -> ImpactAnalysis:
+ """Compute the transitive downstream blast radius of a change.
+
+ Breadth-first, so each artifact is reported at its *shortest* path from the
+ change — the most direct explanation of why it is affected, which is what the
+ impact preview should show a reviewer.
+
+ Cycles are possible in a real project graph (an architecture decision that
+ feeds back into a requirement), so visited artifacts are never re-expanded.
+ The changed artifact is excluded from its own impact set even if a cycle
+ returns to it.
+
+ Args:
+ changed_artifact_id: The artifact being modified.
+ edges: Every edge in the project.
+ max_depth: Optional hop limit, for previewing immediate effects only.
+
+ Returns:
+ The impacted set, ordered by depth then by discovery.
+ """
+ downstream_by_upstream: dict[str, list[TraceEdge]] = {}
+ for edge in edges:
+ downstream_by_upstream.setdefault(edge.upstream_artifact_id, []).append(edge)
+
+ impacted: list[ImpactedArtifact] = []
+ visited: set[str] = {changed_artifact_id}
+ queue: deque[tuple[str, int, list[str]]] = deque(
+ [(changed_artifact_id, 0, [changed_artifact_id])]
+ )
+
+ while queue:
+ artifact_id, depth, path = queue.popleft()
+
+ if max_depth is not None and depth >= max_depth:
+ continue
+
+ for edge in downstream_by_upstream.get(artifact_id, []):
+ target = edge.downstream_artifact_id
+ if target in visited:
+ continue
+
+ visited.add(target)
+ next_path = [*path, target]
+ impacted.append(
+ ImpactedArtifact(
+ artifact_id=target,
+ depth=depth + 1,
+ via_kind=edge.kind,
+ path=next_path,
+ )
+ )
+ queue.append((target, depth + 1, next_path))
+
+ return ImpactAnalysis(changed_artifact_id=changed_artifact_id, impacted=impacted)
+
+
+def upstream_of(
+ artifact_id: str,
+ edges: Iterable[TraceEdge],
+) -> list[TraceEdge]:
+ """Return the edges this artifact depends on.
+
+ The reverse query, and the one that answers "why does this artifact exist?"
+ when a user clicks through the traceability graph.
+ """
+ return [edge for edge in edges if edge.downstream_artifact_id == artifact_id]
diff --git a/submissions/Victorious/apps/api/app/events/__init__.py b/submissions/Victorious/apps/api/app/events/__init__.py
new file mode 100644
index 00000000..8e4da035
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/events/__init__.py
@@ -0,0 +1,5 @@
+"""Event publication and live fan-out."""
+
+from app.events.bus import EventBus
+
+__all__ = ["EventBus"]
diff --git a/submissions/Victorious/apps/api/app/events/bus.py b/submissions/Victorious/apps/api/app/events/bus.py
new file mode 100644
index 00000000..97b2d2af
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/events/bus.py
@@ -0,0 +1,97 @@
+"""In-process event bus.
+
+Publishing an event does two things: it appends to the durable record in shared
+memory, and it fans out to live subscribers. Milestone 6 subscribes the SSE
+endpoint here, so the browser sees agent activity as it happens while the
+timeline can still be reconstructed after a reload.
+
+Subscribers must never be able to break publication. An agent's work is not
+invalidated because a browser connection dropped mid-write, so a failing
+subscriber is logged and skipped.
+"""
+
+from __future__ import annotations
+
+import asyncio
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager
+
+from app.core.logging import get_logger
+from app.domain.events import ProjectEvent
+from app.memory.repository import EventRepository
+
+logger = get_logger(__name__)
+
+#: Bounded so a stalled consumer cannot grow memory without limit. On overflow
+#: the oldest event is dropped for that subscriber only; it reconnects with
+#: ``after_id`` and replays what it missed from the durable record.
+_SUBSCRIBER_QUEUE_SIZE = 256
+
+
+class EventBus:
+ """Durable append plus live fan-out."""
+
+ def __init__(self, events: EventRepository) -> None:
+ self._events = events
+ self._subscribers: dict[str, set[asyncio.Queue[ProjectEvent]]] = {}
+ self._lock = asyncio.Lock()
+
+ async def publish(self, event: ProjectEvent) -> ProjectEvent:
+ """Persist an event, then deliver it to live subscribers.
+
+ Persistence happens first and deliberately: an event that reached a
+ browser but was never recorded would leave the timeline disagreeing with
+ what the user watched happen.
+ """
+ stored = await self._events.append(event)
+
+ async with self._lock:
+ queues = list(self._subscribers.get(event.project_id, ()))
+
+ for queue in queues:
+ try:
+ queue.put_nowait(stored)
+ except asyncio.QueueFull:
+ # Drop the oldest for this subscriber and retry once; it will
+ # reconcile on reconnect.
+ try:
+ queue.get_nowait()
+ queue.put_nowait(stored)
+ except (asyncio.QueueEmpty, asyncio.QueueFull):
+ logger.warning(
+ "Dropped event for slow subscriber",
+ extra={"project_id": event.project_id, "event_type": event.type.value},
+ )
+
+ return stored
+
+ @asynccontextmanager
+ async def subscribe(self, project_id: str) -> AsyncIterator[asyncio.Queue[ProjectEvent]]:
+ """Subscribe to a project's live events for the duration of the block.
+
+ The queue is always unregistered on exit, including when the client
+ disconnects mid-stream, so a dropped browser tab cannot leak a queue that
+ the publisher keeps filling forever.
+ """
+ queue: asyncio.Queue[ProjectEvent] = asyncio.Queue(maxsize=_SUBSCRIBER_QUEUE_SIZE)
+
+ async with self._lock:
+ self._subscribers.setdefault(project_id, set()).add(queue)
+
+ logger.debug("Event subscriber attached", extra={"project_id": project_id})
+
+ try:
+ yield queue
+ finally:
+ async with self._lock:
+ subscribers = self._subscribers.get(project_id)
+ if subscribers is not None:
+ subscribers.discard(queue)
+ if not subscribers:
+ del self._subscribers[project_id]
+
+ logger.debug("Event subscriber detached", extra={"project_id": project_id})
+
+ def subscriber_count(self, project_id: str) -> int:
+ """Return the number of live subscribers. Used by tests and diagnostics."""
+ return len(self._subscribers.get(project_id, ()))
diff --git a/submissions/Victorious/apps/api/app/events/sse.py b/submissions/Victorious/apps/api/app/events/sse.py
new file mode 100644
index 00000000..ef744599
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/events/sse.py
@@ -0,0 +1,68 @@
+"""Server-Sent Events framing.
+
+ADR-0005 chose SSE over WebSockets: agent activity is strictly one-directional —
+the server emits, the browser renders — and SSE does that over plain HTTP with
+automatic client reconnection and no extra infrastructure.
+
+This module owns the wire format only. Deciding *what* to stream belongs to the
+router; deciding *when* belongs to the event bus.
+"""
+
+from __future__ import annotations
+
+import json
+
+from app.domain.events import ProjectEvent
+
+#: How long the stream may sit silent before emitting a comment frame.
+#:
+#: Proxies and load balancers close idle connections, and a demo that silently
+#: stops updating after a minute of an agent thinking is worse than no stream at
+#: all. Comment frames are ignored by EventSource but keep the socket alive.
+HEARTBEAT_SECONDS = 15.0
+
+#: Reconnection delay advertised to the browser, in milliseconds. EventSource
+#: reconnects on its own; this only tunes how eagerly.
+RETRY_MS = 3000
+
+
+def format_event(event: ProjectEvent) -> str:
+ """Render a project event as one SSE frame.
+
+ The ``id`` field is the event's own identifier, which the browser echoes back
+ as ``Last-Event-ID`` when it reconnects. That is what makes resumption exact
+ rather than approximate — the same cursor the durable event log already
+ understands.
+ """
+ payload = {
+ "id": event.id,
+ "type": event.type.value,
+ "stage": event.stage.value if event.stage else None,
+ "role": event.role.value if event.role else None,
+ "summary": event.summary,
+ "payload": event.payload,
+ "created_at": event.created_at.isoformat(),
+ }
+
+ # `event:` names the frame so the client can listen selectively rather than
+ # parsing every message to discover it does not care about it.
+ return (
+ f"id: {event.id}\n"
+ f"event: {event.type.value}\n"
+ f"data: {json.dumps(payload, default=str)}\n\n"
+ )
+
+
+def format_heartbeat() -> str:
+ """A comment frame. Keeps the connection open without reaching the client."""
+ return ": heartbeat\n\n"
+
+
+def format_retry() -> str:
+ """Advertise the reconnection delay. Sent once when the stream opens."""
+ return f"retry: {RETRY_MS}\n\n"
+
+
+def format_open() -> str:
+ """A frame marking the stream ready, so the UI can show it is connected."""
+ return 'event: stream_open\ndata: {"connected":true}\n\n'
diff --git a/submissions/Victorious/apps/api/app/llm/__init__.py b/submissions/Victorious/apps/api/app/llm/__init__.py
new file mode 100644
index 00000000..f8806494
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/__init__.py
@@ -0,0 +1,35 @@
+"""Reasoning provider abstraction.
+
+Agents depend on :class:`LLMProvider`; the composition root chooses the adapter.
+No module outside this package imports a vendor SDK (ADR-0004).
+"""
+
+from app.llm.fixture_provider import FixtureProvider, fixture_name
+from app.llm.provider import (
+ CompletionRequest,
+ CompletionResponse,
+ LLMProvider,
+ Message,
+ Role,
+ StructuredResponse,
+)
+from app.llm.recording import RecordingProvider
+from app.llm.registry import ProviderHealthCheck, build_provider
+from app.llm.retry import SchemaViolationError, TransientProviderError, with_retries
+
+__all__ = [
+ "CompletionRequest",
+ "CompletionResponse",
+ "FixtureProvider",
+ "LLMProvider",
+ "Message",
+ "ProviderHealthCheck",
+ "RecordingProvider",
+ "Role",
+ "SchemaViolationError",
+ "StructuredResponse",
+ "TransientProviderError",
+ "build_provider",
+ "fixture_name",
+ "with_retries",
+]
diff --git a/submissions/Victorious/apps/api/app/llm/anthropic_provider.py b/submissions/Victorious/apps/api/app/llm/anthropic_provider.py
new file mode 100644
index 00000000..3e6105fb
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/anthropic_provider.py
@@ -0,0 +1,247 @@
+"""Anthropic (Claude) reasoning adapter — the default provider per ADR-0004.
+
+Structured output uses forced tool use rather than asking for JSON in the prompt.
+The model is given a single tool whose input schema is the agent's output
+contract and is required to call it, so the API constrains generation to the
+schema instead of the platform hoping prose happens to parse. That reliability is
+the reason ADR-0004 makes this the default: an agent returning malformed output
+does not degrade gracefully, it halts a lifecycle stage.
+"""
+
+from __future__ import annotations
+
+import json
+from collections.abc import AsyncIterator
+from typing import Any
+
+from pydantic import BaseModel, ValidationError
+
+from app.core.config import LLMSettings
+from app.core.logging import get_logger
+from app.domain.agents import TokenUsage
+from app.domain.errors import ProviderError
+from app.llm.provider import (
+ CompletionRequest,
+ CompletionResponse,
+ Message,
+ Role,
+ StructuredResponse,
+)
+from app.llm.retry import SchemaViolationError, TransientProviderError, with_retries
+
+logger = get_logger(__name__)
+
+_STRUCTURED_TOOL_NAME = "emit_engineering_output"
+
+#: Vendor exception names worth retrying unchanged. Anything else — bad request,
+#: authentication failure — fails identically on retry and is raised at once.
+_TRANSIENT_ERRORS = frozenset(
+ {
+ "RateLimitError",
+ "APITimeoutError",
+ "APIConnectionError",
+ "InternalServerError",
+ "APIStatusError",
+ }
+)
+
+
+class AnthropicProvider:
+ """Reasoning backed by the Anthropic Messages API."""
+
+ def __init__(self, settings: LLMSettings) -> None:
+ if not settings.anthropic_api_key:
+ raise ProviderError(
+ "Anthropic provider selected but no API key is configured",
+ details={"env_var": "ANTHROPIC_API_KEY"},
+ )
+
+ # Imported lazily so the SDK is only required when this provider is
+ # actually selected — the fixture provider must work with no vendor SDK
+ # installed at all.
+ from anthropic import AsyncAnthropic
+
+ self._settings = settings
+ self._client = AsyncAnthropic(
+ api_key=settings.anthropic_api_key,
+ timeout=settings.timeout_seconds,
+ max_retries=0, # Retry policy is ours (app/llm/retry.py), not the SDK's.
+ )
+
+ @property
+ def name(self) -> str:
+ return "anthropic"
+
+ @property
+ def model(self) -> str:
+ return self._settings.anthropic_model
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ async def attempt(_: int) -> CompletionResponse:
+ message = await self._send(request)
+ text = "".join(
+ block.text
+ for block in message.content
+ if getattr(block, "type", None) == "text"
+ )
+ return CompletionResponse(
+ text=text,
+ usage=_usage_of(message),
+ model=self.model,
+ provider=self.name,
+ )
+
+ return await with_retries(
+ attempt,
+ max_retries=self._settings.max_retries,
+ description="anthropic.complete",
+ )
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ tool: dict[str, Any] = {
+ "name": _STRUCTURED_TOOL_NAME,
+ "description": "Emit the engineering output for this task. Every field is required.",
+ "input_schema": schema.model_json_schema(),
+ }
+
+ # Held in the closure rather than on the instance: a provider is a
+ # shared singleton, and instance state would let two concurrent agents
+ # overwrite each other's correction message.
+ last_violation: str | None = None
+
+ async def attempt(attempt_number: int) -> StructuredResponse[T]:
+ nonlocal last_violation
+
+ # A schema violation is not transient. Repeating the identical
+ # request repeats the identical mistake, so the retry carries the
+ # validation error back to the model.
+ current = request
+ if attempt_number > 0 and last_violation is not None:
+ current = _with_correction(request, last_violation)
+
+ message = await self._send(current, tool=tool)
+
+ tool_use = next(
+ (block for block in message.content if getattr(block, "type", None) == "tool_use"),
+ None,
+ )
+ if tool_use is None:
+ last_violation = "No tool call was emitted; the tool must be called."
+ raise SchemaViolationError(
+ "Anthropic returned no structured output",
+ details={"schema": schema.__name__},
+ )
+
+ try:
+ value = schema.model_validate(tool_use.input)
+ except ValidationError as exc:
+ last_violation = exc.json(include_url=False)
+ raise SchemaViolationError(
+ "Anthropic output failed schema validation",
+ details={"schema": schema.__name__, "error_count": exc.error_count()},
+ ) from exc
+
+ return StructuredResponse(
+ value=value,
+ raw_json=json.dumps(tool_use.input),
+ usage=_usage_of(message),
+ model=self.model,
+ provider=self.name,
+ )
+
+ return await with_retries(
+ attempt,
+ max_retries=self._settings.max_retries,
+ description=f"anthropic.complete_structured[{schema.__name__}]",
+ )
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ try:
+ async with self._client.messages.stream(
+ model=self.model,
+ system=request.system,
+ # The SDK types this as a TypedDict union; our dicts satisfy it
+ # structurally, but the roles are only known at runtime.
+ messages=_to_messages(request), # type: ignore[arg-type]
+ max_tokens=request.max_tokens,
+ temperature=request.temperature,
+ ) as stream:
+ async for chunk in stream.text_stream:
+ yield chunk
+ except Exception as exc:
+ raise _translate(exc) from exc
+
+ async def aclose(self) -> None:
+ await self._client.close()
+
+ async def _send(
+ self, request: CompletionRequest, *, tool: dict[str, Any] | None = None
+ ) -> Any:
+ kwargs: dict[str, Any] = {
+ "model": self.model,
+ "system": request.system,
+ "messages": _to_messages(request),
+ "max_tokens": request.max_tokens,
+ "temperature": request.temperature,
+ }
+ if tool is not None:
+ kwargs["tools"] = [tool]
+ kwargs["tool_choice"] = {"type": "tool", "name": _STRUCTURED_TOOL_NAME}
+
+ try:
+ return await self._client.messages.create(**kwargs)
+ except Exception as exc:
+ raise _translate(exc) from exc
+
+
+def _to_messages(request: CompletionRequest) -> list[dict[str, str]]:
+ return [
+ {"role": message.role.value, "content": message.content}
+ for message in request.messages
+ ]
+
+
+def _with_correction(request: CompletionRequest, violation: str) -> CompletionRequest:
+ """Return the request with the validation failure appended for the retry."""
+ return CompletionRequest(
+ system=request.system,
+ messages=[
+ *request.messages,
+ Message(
+ role=Role.USER,
+ content=(
+ "Your previous output failed schema validation:\n"
+ f"{violation}\n\n"
+ "Call the tool again with corrected output."
+ ),
+ ),
+ ],
+ max_tokens=request.max_tokens,
+ temperature=request.temperature,
+ fixture_key=request.fixture_key,
+ metadata=request.metadata,
+ )
+
+
+def _usage_of(message: Any) -> TokenUsage:
+ usage = getattr(message, "usage", None)
+ if usage is None:
+ return TokenUsage()
+ return TokenUsage(
+ input_tokens=getattr(usage, "input_tokens", 0) or 0,
+ output_tokens=getattr(usage, "output_tokens", 0) or 0,
+ )
+
+
+def _translate(exc: Exception) -> ProviderError:
+ """Convert a vendor exception into a domain error."""
+ name = type(exc).__name__
+
+ if name in _TRANSIENT_ERRORS:
+ return TransientProviderError(
+ f"Anthropic request failed transiently: {name}", details={"error_type": name}
+ )
+
+ return ProviderError(f"Anthropic request failed: {name}", details={"error_type": name})
diff --git a/submissions/Victorious/apps/api/app/llm/fixture_provider.py b/submissions/Victorious/apps/api/app/llm/fixture_provider.py
new file mode 100644
index 00000000..3f0ab262
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/fixture_provider.py
@@ -0,0 +1,216 @@
+"""Fixture provider — replays recorded reasoning from disk.
+
+A first-class provider, not a mock. It exists for two reasons the specification
+makes explicit:
+
+- **Demo resilience.** `12_Risk_Analysis.md` rates Model Availability a Medium
+ risk, and `13_Demo_and_Pitch.md` requires a polished end-to-end demonstration.
+ A provider outage during a live demo is otherwise unrecoverable. With recorded
+ fixtures the entire platform runs with no network at all.
+- **Deterministic tests.** The suite exercises real agent code paths without
+ network access, latency, or API spend.
+
+Fixtures are plain JSON on disk with human-readable names. A reviewer can open,
+read, and edit one — which a content-hash filename would prevent.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import re
+from collections.abc import AsyncIterator
+from pathlib import Path
+from typing import Any
+
+from pydantic import BaseModel, ValidationError
+
+from app.core.logging import get_logger
+from app.domain.agents import TokenUsage
+from app.domain.errors import ProviderError
+from app.llm.provider import CompletionRequest, CompletionResponse, StructuredResponse
+
+logger = get_logger(__name__)
+
+#: Streaming replays in slices of this size so the UI exercises its incremental
+#: rendering path during a fixture-backed demo rather than receiving one blob.
+_STREAM_CHUNK_CHARS = 48
+
+#: Placeholder a fixture uses in place of project-specific upstream artifact IDs.
+UPSTREAM_TOKEN = "$upstream" # noqa: S105 - a substitution token, not a credential
+
+_ARTIFACT_ID = re.compile(r"art_[0-9a-f]{32}")
+
+
+class FixtureProvider:
+ """Replays recorded provider responses from a directory."""
+
+ def __init__(self, fixture_dir: str | Path, *, model: str = "fixture") -> None:
+ self._dir = Path(fixture_dir)
+ self._model = model
+
+ @property
+ def name(self) -> str:
+ return "fixture"
+
+ @property
+ def model(self) -> str:
+ return self._model
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ payload = self._load(request)
+ text = payload.get("text")
+ if not isinstance(text, str):
+ raise ProviderError(
+ "Fixture is missing a string 'text' field",
+ details={"fixture": self._path_for(request).name},
+ )
+
+ return CompletionResponse(
+ text=text,
+ usage=_usage_of(payload),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ payload = self._load(request)
+ value_data = payload.get("value")
+
+ if value_data is None:
+ raise ProviderError(
+ "Fixture is missing a 'value' object for structured output",
+ details={"fixture": self._path_for(request).name, "schema": schema.__name__},
+ )
+
+ if isinstance(value_data, dict):
+ value_data = _expand_upstream(value_data, request)
+
+ try:
+ value = schema.model_validate(value_data)
+ except ValidationError as exc:
+ # A stale fixture is a real failure worth surfacing loudly: it means
+ # the agent's contract changed and the recording was not refreshed,
+ # which would otherwise show up as inexplicable demo behaviour.
+ raise ProviderError(
+ "Recorded fixture no longer matches the agent output contract",
+ details={
+ "fixture": self._path_for(request).name,
+ "schema": schema.__name__,
+ "error_count": exc.error_count(),
+ },
+ ) from exc
+
+ return StructuredResponse(
+ value=value,
+ raw_json=json.dumps(value_data),
+ usage=_usage_of(payload),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ response = await self.complete(request)
+ for start in range(0, len(response.text), _STREAM_CHUNK_CHARS):
+ yield response.text[start : start + _STREAM_CHUNK_CHARS]
+
+ async def aclose(self) -> None:
+ return None
+
+ def _load(self, request: CompletionRequest) -> dict[str, Any]:
+ path = self._path_for(request)
+
+ if not path.is_file():
+ raise ProviderError(
+ "No recorded fixture for this request",
+ details={
+ "fixture": path.name,
+ "directory": str(self._dir),
+ "hint": (
+ "Record fixtures by running with a live provider and "
+ "VICTORIOUS_LLM__RECORD_FIXTURES=true."
+ ),
+ },
+ )
+
+ try:
+ loaded = json.loads(path.read_text(encoding="utf-8"))
+ except json.JSONDecodeError as exc:
+ raise ProviderError(
+ "Fixture file is not valid JSON", details={"fixture": path.name}
+ ) from exc
+
+ if not isinstance(loaded, dict):
+ raise ProviderError(
+ "Fixture must be a JSON object", details={"fixture": path.name}
+ )
+ return loaded
+
+ def _path_for(self, request: CompletionRequest) -> Path:
+ return self._dir / f"{fixture_name(request)}.json"
+
+
+def fixture_name(request: CompletionRequest) -> str:
+ """Return the filename stem for a request.
+
+ Prefers the explicit ``fixture_key`` an agent supplies, so demo fixtures are
+ named after the work they represent. Falls back to a content hash only when
+ no key is given, which keeps ad-hoc calls recordable without inventing names.
+ """
+ if request.fixture_key:
+ return request.fixture_key
+
+ digest = hashlib.sha256()
+ digest.update(request.system.encode("utf-8"))
+ for message in request.messages:
+ digest.update(message.role.value.encode("utf-8"))
+ digest.update(message.content.encode("utf-8"))
+ return f"anon_{digest.hexdigest()[:16]}"
+
+
+def _expand_upstream(value: dict[str, Any], request: CompletionRequest) -> dict[str, Any]:
+ """Resolve the ``$upstream`` token in a recorded ``sources`` field.
+
+ Artifact IDs are minted per project, so a recording made against one project
+ cites IDs that exist in no other. Without substitution a replayed fixture
+ could never declare its upstream, and the agent base class would reject every
+ downstream artifact as an orphan — making an offline demo impossible.
+
+ A fixture therefore records ``"sources": "$upstream"``, and this expands it to
+ the artifact IDs actually present in the current request's context. The
+ resulting edges are truthful: those are the artifacts the agent was shown.
+ """
+ if value.get("sources") != UPSTREAM_TOKEN:
+ return value
+
+ context = " ".join(message.content for message in request.messages)
+ artifact_ids = sorted(set(_ARTIFACT_ID.findall(context)))
+
+ return {
+ **value,
+ "sources": [
+ {
+ "upstream_artifact_id": artifact_id,
+ "kind": "derives_from",
+ "rationale": "Supplied as upstream engineering context for this stage.",
+ }
+ for artifact_id in artifact_ids
+ ],
+ }
+
+
+def _usage_of(payload: dict[str, Any]) -> TokenUsage:
+ """Read recorded usage, defaulting to zero.
+
+ Recorded counts are preserved so a fixture-backed demo still shows realistic
+ token figures on the agent cards rather than zeros.
+ """
+ usage = payload.get("usage")
+ if not isinstance(usage, dict):
+ return TokenUsage()
+ return TokenUsage(
+ input_tokens=int(usage.get("input_tokens", 0)),
+ output_tokens=int(usage.get("output_tokens", 0)),
+ )
diff --git a/submissions/Victorious/apps/api/app/llm/gemini_provider.py b/submissions/Victorious/apps/api/app/llm/gemini_provider.py
new file mode 100644
index 00000000..591008e8
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/gemini_provider.py
@@ -0,0 +1,223 @@
+"""Google Gemini reasoning adapter.
+
+`08_Technology_Stack.md` names Gemini as the LLM. ADR-0004 makes Claude the
+default on structured-output reliability grounds while keeping Gemini a real,
+exercised adapter — provider agnosticism that is never run against a second
+provider is a claim, not a property.
+
+Structured output uses the API's native JSON mode with a response schema, which
+is Gemini's equivalent of constraining generation rather than hoping prose parses.
+"""
+
+from __future__ import annotations
+
+import json
+from collections.abc import AsyncIterator
+from typing import Any
+
+from pydantic import BaseModel, ValidationError
+
+from app.core.config import LLMSettings
+from app.core.logging import get_logger
+from app.domain.agents import TokenUsage
+from app.domain.errors import ProviderError
+from app.llm.provider import (
+ CompletionRequest,
+ CompletionResponse,
+ Message,
+ Role,
+ StructuredResponse,
+)
+from app.llm.retry import SchemaViolationError, TransientProviderError, with_retries
+
+logger = get_logger(__name__)
+
+#: Substrings of vendor error messages that indicate a transient condition.
+#: Matched on text because the SDK raises a small number of broad exception
+#: types and encodes the distinguishing detail in the message.
+_TRANSIENT_MARKERS = ("429", "500", "502", "503", "504", "timeout", "deadline", "unavailable")
+
+
+class GeminiProvider:
+ """Reasoning backed by the Google Gen AI API."""
+
+ def __init__(self, settings: LLMSettings) -> None:
+ if not settings.google_api_key:
+ raise ProviderError(
+ "Gemini provider selected but no API key is configured",
+ details={"env_var": "GOOGLE_API_KEY"},
+ )
+
+ # Lazy, as in the Anthropic adapter: selecting one provider must not
+ # require the other's SDK to be installed.
+ from google import genai
+
+ self._settings = settings
+ self._client = genai.Client(api_key=settings.google_api_key)
+
+ @property
+ def name(self) -> str:
+ return "gemini"
+
+ @property
+ def model(self) -> str:
+ return self._settings.gemini_model
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ async def attempt(_: int) -> CompletionResponse:
+ response = await self._send(request)
+ return CompletionResponse(
+ text=response.text or "",
+ usage=_usage_of(response),
+ model=self.model,
+ provider=self.name,
+ )
+
+ return await with_retries(
+ attempt, max_retries=self._settings.max_retries, description="gemini.complete"
+ )
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ last_violation: str | None = None
+
+ async def attempt(attempt_number: int) -> StructuredResponse[T]:
+ nonlocal last_violation
+
+ current = request
+ if attempt_number > 0 and last_violation is not None:
+ current = _with_correction(request, last_violation)
+
+ response = await self._send(current, schema=schema)
+ raw = response.text or ""
+
+ try:
+ value = schema.model_validate_json(raw)
+ except ValidationError as exc:
+ last_violation = exc.json(include_url=False)
+ raise SchemaViolationError(
+ "Gemini output failed schema validation",
+ details={"schema": schema.__name__, "error_count": exc.error_count()},
+ ) from exc
+ except json.JSONDecodeError as exc:
+ last_violation = f"Output was not valid JSON: {exc}"
+ raise SchemaViolationError(
+ "Gemini returned malformed JSON", details={"schema": schema.__name__}
+ ) from exc
+
+ return StructuredResponse(
+ value=value,
+ raw_json=raw,
+ usage=_usage_of(response),
+ model=self.model,
+ provider=self.name,
+ )
+
+ return await with_retries(
+ attempt,
+ max_retries=self._settings.max_retries,
+ description=f"gemini.complete_structured[{schema.__name__}]",
+ )
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ from google.genai import types
+
+ try:
+ stream = await self._client.aio.models.generate_content_stream(
+ model=self.model,
+ contents=_to_contents(request),
+ config=types.GenerateContentConfig(
+ system_instruction=request.system,
+ max_output_tokens=request.max_tokens,
+ temperature=request.temperature,
+ ),
+ )
+ async for chunk in stream:
+ if chunk.text:
+ yield chunk.text
+ except Exception as exc:
+ raise _translate(exc) from exc
+
+ async def aclose(self) -> None:
+ """No-op: the Gen AI client holds no connection pool needing disposal."""
+ return None
+
+ async def _send(
+ self, request: CompletionRequest, *, schema: type[BaseModel] | None = None
+ ) -> Any:
+ from google.genai import types
+
+ config: dict[str, Any] = {
+ "system_instruction": request.system,
+ "max_output_tokens": request.max_tokens,
+ "temperature": request.temperature,
+ }
+ if schema is not None:
+ config["response_mime_type"] = "application/json"
+ config["response_schema"] = schema
+
+ try:
+ return await self._client.aio.models.generate_content(
+ model=self.model,
+ contents=_to_contents(request),
+ config=types.GenerateContentConfig(**config),
+ )
+ except Exception as exc:
+ raise _translate(exc) from exc
+
+
+def _to_contents(request: CompletionRequest) -> list[dict[str, Any]]:
+ """Map the shared message shape onto Gemini's content format.
+
+ Gemini names the assistant role "model"; the mapping is confined here so no
+ caller has to know that.
+ """
+ role_map = {Role.USER: "user", Role.ASSISTANT: "model"}
+ return [
+ {"role": role_map[message.role], "parts": [{"text": message.content}]}
+ for message in request.messages
+ ]
+
+
+def _with_correction(request: CompletionRequest, violation: str) -> CompletionRequest:
+ return CompletionRequest(
+ system=request.system,
+ messages=[
+ *request.messages,
+ Message(
+ role=Role.USER,
+ content=(
+ "Your previous output failed schema validation:\n"
+ f"{violation}\n\n"
+ "Return corrected JSON matching the schema exactly."
+ ),
+ ),
+ ],
+ max_tokens=request.max_tokens,
+ temperature=request.temperature,
+ fixture_key=request.fixture_key,
+ metadata=request.metadata,
+ )
+
+
+def _usage_of(response: Any) -> TokenUsage:
+ metadata = getattr(response, "usage_metadata", None)
+ if metadata is None:
+ return TokenUsage()
+ return TokenUsage(
+ input_tokens=getattr(metadata, "prompt_token_count", 0) or 0,
+ output_tokens=getattr(metadata, "candidates_token_count", 0) or 0,
+ )
+
+
+def _translate(exc: Exception) -> ProviderError:
+ message = str(exc).lower()
+ name = type(exc).__name__
+
+ if any(marker in message for marker in _TRANSIENT_MARKERS):
+ return TransientProviderError(
+ f"Gemini request failed transiently: {name}", details={"error_type": name}
+ )
+
+ return ProviderError(f"Gemini request failed: {name}", details={"error_type": name})
diff --git a/submissions/Victorious/apps/api/app/llm/provider.py b/submissions/Victorious/apps/api/app/llm/provider.py
new file mode 100644
index 00000000..f33be33d
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/provider.py
@@ -0,0 +1,138 @@
+"""Reasoning provider abstraction.
+
+`15_Development_Guidelines.md` requires the platform to stay AI-provider agnostic
+and warns against coupling implementation to a single language model.
+`12_Risk_Analysis.md` rates Model Availability a Medium risk mitigated by a
+provider abstraction layer and multiple providers.
+
+This module is that layer. No agent imports a vendor SDK; every agent depends on
+:class:`LLMProvider`, and the composition root decides which adapter backs it
+(ADR-0004).
+
+The interface is deliberately narrow. Agents need text generation and validated
+structured output — nothing exotic — and a wide interface would only constrain
+which providers can implement it.
+"""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+from dataclasses import dataclass, field
+from enum import StrEnum
+from typing import Protocol, runtime_checkable
+
+from pydantic import BaseModel
+
+from app.domain.agents import TokenUsage
+
+
+class Role(StrEnum):
+ """Conversation roles common to every supported provider."""
+
+ USER = "user"
+ ASSISTANT = "assistant"
+
+
+@dataclass(frozen=True)
+class Message:
+ """One turn of a conversation."""
+
+ role: Role
+ content: str
+
+
+@dataclass(frozen=True)
+class CompletionRequest:
+ """A request for reasoning.
+
+ ``fixture_key`` is carried on the request rather than derived inside the
+ fixture provider so recorded responses have stable, human-readable filenames
+ (``product_manager.requirement_discovery.json``) that a reviewer can open,
+ read, and edit. A content hash would make the demo fixtures opaque.
+ """
+
+ system: str
+ messages: list[Message]
+ max_tokens: int = 8192
+ temperature: float = 0.2
+ fixture_key: str | None = None
+ metadata: dict[str, str] = field(default_factory=dict)
+
+
+@dataclass(frozen=True)
+class CompletionResponse:
+ """Free-text reasoning output."""
+
+ text: str
+ usage: TokenUsage
+ model: str
+ provider: str
+
+
+@dataclass(frozen=True)
+class StructuredResponse[T: BaseModel]:
+ """Reasoning output validated against an agent's output contract."""
+
+ value: T
+ raw_json: str
+ usage: TokenUsage
+ model: str
+ provider: str
+
+
+@runtime_checkable
+class LLMProvider(Protocol):
+ """A reasoning backend.
+
+ Implementations must translate vendor failures into
+ :class:`app.domain.errors.ProviderError`, so retry, fallback, and degradation
+ policy stay inside the platform rather than leaking a vendor exception into
+ orchestration code.
+ """
+
+ @property
+ def name(self) -> str:
+ """Stable provider identifier, recorded on every agent run."""
+ ...
+
+ @property
+ def model(self) -> str:
+ """Model identifier, recorded on every agent run."""
+ ...
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ """Generate free-text output."""
+ ...
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ """Generate output validated against ``schema``.
+
+ The path every agent uses. Downstream agents read structured content
+ rather than parsing prose, so a provider that cannot reliably produce
+ valid instances of a schema is not usable here — which is why ADR-0004
+ makes structured-output reliability the criterion for the default.
+
+ Raises:
+ ProviderError: on transport failure, or when output cannot be
+ validated against the schema after retries.
+ """
+ ...
+
+ # Declared without `async` deliberately: implementations are async
+ # generators, whose type is a callable returning an AsyncIterator rather than
+ # a coroutine that resolves to one. Marking this `async def` would make every
+ # real adapter fail the protocol check.
+ def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ """Yield output incrementally.
+
+ Used by Milestone 6 to surface agent reasoning as it is produced rather
+ than after completion, which `10_UI_UX_Plan.md` requires instead of
+ hiding agents behind loading indicators.
+ """
+ ...
+
+ async def aclose(self) -> None:
+ """Release any underlying client. Invoked by the container on shutdown."""
+ ...
diff --git a/submissions/Victorious/apps/api/app/llm/recording.py b/submissions/Victorious/apps/api/app/llm/recording.py
new file mode 100644
index 00000000..c0ae1f43
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/recording.py
@@ -0,0 +1,109 @@
+"""Fixture recorder.
+
+Wraps a live provider and writes every response to disk in the format
+:mod:`app.llm.fixture_provider` replays. Recording once against a real provider
+is what makes the offline demo possible.
+
+A decorator rather than a flag inside each adapter: recording is orthogonal to
+which provider is in use, and putting it here means both adapters — and any
+future one — gain it for free.
+"""
+
+from __future__ import annotations
+
+import json
+from collections.abc import AsyncIterator
+from pathlib import Path
+
+from pydantic import BaseModel
+
+from app.core.logging import get_logger
+from app.llm.fixture_provider import fixture_name
+from app.llm.provider import (
+ CompletionRequest,
+ CompletionResponse,
+ LLMProvider,
+ StructuredResponse,
+)
+
+logger = get_logger(__name__)
+
+
+class RecordingProvider:
+ """Delegates to a real provider and records what it returns."""
+
+ def __init__(self, inner: LLMProvider, fixture_dir: str | Path) -> None:
+ self._inner = inner
+ self._dir = Path(fixture_dir)
+
+ @property
+ def name(self) -> str:
+ return self._inner.name
+
+ @property
+ def model(self) -> str:
+ return self._inner.model
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ response = await self._inner.complete(request)
+ self._write(
+ request,
+ {
+ "text": response.text,
+ "usage": {
+ "input_tokens": response.usage.input_tokens,
+ "output_tokens": response.usage.output_tokens,
+ },
+ "recorded_from": {"provider": response.provider, "model": response.model},
+ },
+ )
+ return response
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ response = await self._inner.complete_structured(request, schema)
+ self._write(
+ request,
+ {
+ "value": response.value.model_dump(mode="json"),
+ "usage": {
+ "input_tokens": response.usage.input_tokens,
+ "output_tokens": response.usage.output_tokens,
+ },
+ "recorded_from": {
+ "provider": response.provider,
+ "model": response.model,
+ "schema": schema.__name__,
+ },
+ },
+ )
+ return response
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ """Pass streaming through, accumulating the text to record on completion."""
+ chunks: list[str] = []
+
+ async for chunk in self._inner.stream(request):
+ chunks.append(chunk)
+ yield chunk
+
+ self._write(request, {"text": "".join(chunks), "recorded_from": {"stream": True}})
+
+ async def aclose(self) -> None:
+ await self._inner.aclose()
+
+ def _write(self, request: CompletionRequest, payload: dict[str, object]) -> None:
+ """Write a fixture.
+
+ Recording must never break the call it is observing: a read-only
+ directory or a full disk is a recording problem, not an engineering one,
+ so failures are logged and swallowed.
+ """
+ try:
+ self._dir.mkdir(parents=True, exist_ok=True)
+ path = self._dir / f"{fixture_name(request)}.json"
+ path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
+ logger.info("Recorded fixture", extra={"fixture": path.name})
+ except OSError:
+ logger.exception("Failed to record fixture", extra={"directory": str(self._dir)})
diff --git a/submissions/Victorious/apps/api/app/llm/registry.py b/submissions/Victorious/apps/api/app/llm/registry.py
new file mode 100644
index 00000000..1024d7fa
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/registry.py
@@ -0,0 +1,110 @@
+"""Provider construction and fallback.
+
+One place decides which adapter is built and what happens when it cannot be.
+That keeps the composition root declarative and gives `12_Risk_Analysis.md`'s
+"graceful fallback strategies" a concrete implementation rather than a promise.
+"""
+
+from __future__ import annotations
+
+from app.core.config import LLMProvider as ProviderName
+from app.core.config import LLMSettings
+from app.core.health import ComponentHealth, HealthStatus
+from app.core.logging import get_logger
+from app.domain.errors import ProviderError
+from app.llm.fixture_provider import FixtureProvider
+from app.llm.provider import LLMProvider
+from app.llm.recording import RecordingProvider
+
+logger = get_logger(__name__)
+
+
+def build_provider(settings: LLMSettings) -> LLMProvider:
+ """Construct the configured provider.
+
+ Falls back to the fixture provider when a live provider cannot be built —
+ almost always a missing API key. Failing to start would be the wrong
+ behaviour: the platform is fully explorable on recorded fixtures, and a
+ developer who has not yet obtained a key should still be able to run it.
+
+ The fallback is logged at warning level, and the provider name recorded on
+ every agent run makes it unmistakable which backend actually reasoned.
+ """
+ if settings.provider is ProviderName.FIXTURE:
+ return FixtureProvider(settings.fixture_dir)
+
+ try:
+ provider = _build_live(settings)
+ except ProviderError as exc:
+ logger.warning(
+ "Falling back to recorded fixtures",
+ extra={"requested_provider": settings.provider.value, "reason": exc.message},
+ )
+ return FixtureProvider(settings.fixture_dir)
+
+ if settings.record_fixtures:
+ logger.info("Fixture recording enabled", extra={"directory": settings.fixture_dir})
+ return RecordingProvider(provider, settings.fixture_dir)
+
+ return provider
+
+
+def _build_live(settings: LLMSettings) -> LLMProvider:
+ """Construct a network-backed provider.
+
+ Adapters are imported inside the branch so selecting one provider never
+ requires the other's SDK to be installed.
+ """
+ if settings.provider is ProviderName.ANTHROPIC:
+ from app.llm.anthropic_provider import AnthropicProvider
+
+ return AnthropicProvider(settings)
+
+ if settings.provider is ProviderName.GEMINI:
+ from app.llm.gemini_provider import GeminiProvider
+
+ return GeminiProvider(settings)
+
+ raise ProviderError(
+ "Unsupported reasoning provider", details={"provider": settings.provider.value}
+ )
+
+
+class ProviderHealthCheck:
+ """Reports which reasoning backend is live.
+
+ Deliberately does not call the provider: a readiness probe that spends tokens
+ on every poll would be expensive and would count against rate limits. It
+ reports what is configured and whether the platform fell back to fixtures,
+ which is the operationally important fact.
+ """
+
+ def __init__(self, provider: LLMProvider, settings: LLMSettings) -> None:
+ self._provider = provider
+ self._settings = settings
+
+ @property
+ def name(self) -> str:
+ return "reasoning_provider"
+
+ @property
+ def critical(self) -> bool:
+ """Non-critical: on fixtures the platform still serves every read path."""
+ return False
+
+ async def check(self) -> ComponentHealth:
+ configured = self._settings.provider.value
+ active = self._provider.name
+
+ if active == configured:
+ return ComponentHealth(
+ name=self.name,
+ status=HealthStatus.HEALTHY,
+ message=f"{active} · {self._provider.model}",
+ )
+
+ return ComponentHealth(
+ name=self.name,
+ status=HealthStatus.DEGRADED,
+ message=f"Configured for {configured}, running on recorded fixtures",
+ )
diff --git a/submissions/Victorious/apps/api/app/llm/retry.py b/submissions/Victorious/apps/api/app/llm/retry.py
new file mode 100644
index 00000000..d6b0a4c4
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/llm/retry.py
@@ -0,0 +1,105 @@
+"""Retry policy for provider calls.
+
+Implemented here rather than per adapter so every provider degrades identically,
+and so the policy is one auditable thing rather than three.
+
+Two failure classes are distinguished deliberately:
+
+- **Transport failures** (rate limits, timeouts, 5xx) are transient. Retrying the
+ identical request is correct.
+- **Schema violations** — valid transport, output that will not validate — are
+ not transient. Retrying the identical request repeats the same mistake, so the
+ caller supplies a corrected request carrying the validation error, giving the
+ model the information it needs to do better.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import random
+from collections.abc import Awaitable, Callable
+
+from app.core.logging import get_logger
+from app.domain.errors import ProviderError
+
+logger = get_logger(__name__)
+
+_BASE_DELAY_SECONDS = 0.5
+_MAX_DELAY_SECONDS = 8.0
+
+
+class TransientProviderError(ProviderError):
+ """A provider failure worth retrying unchanged."""
+
+ code = "provider_transient_error"
+
+
+class SchemaViolationError(ProviderError):
+ """Provider output could not be validated against the requested schema."""
+
+ code = "provider_schema_violation"
+
+
+def backoff_delay(attempt: int) -> float:
+ """Return the delay before ``attempt`` (0-based), with jitter.
+
+ Jitter matters under concurrency: seven agents retrying on the same schedule
+ would resynchronise into a thundering herd against the same rate limit.
+ """
+ delay: float = min(_BASE_DELAY_SECONDS * float(2**attempt), _MAX_DELAY_SECONDS)
+ jitter: float = 0.5 + random.random() / 2 # noqa: S311 - jitter, not crypto
+ return delay * jitter
+
+
+async def with_retries[R](
+ operation: Callable[[int], Awaitable[R]],
+ *,
+ max_retries: int,
+ description: str,
+) -> R:
+ """Run ``operation`` until it succeeds or retries are exhausted.
+
+ Args:
+ operation: Receives the 0-based attempt number, so a caller can vary the
+ request between attempts — which is how schema violations are
+ corrected rather than merely repeated.
+ max_retries: Additional attempts after the first.
+ description: Included in logs and in the final error.
+
+ Returns:
+ The operation's result.
+
+ Raises:
+ ProviderError: when every attempt fails. The last failure is the cause.
+ """
+ last_error: ProviderError | None = None
+
+ for attempt in range(max_retries + 1):
+ try:
+ return await operation(attempt)
+ except (TransientProviderError, SchemaViolationError) as exc:
+ last_error = exc
+
+ if attempt == max_retries:
+ break
+
+ delay = backoff_delay(attempt)
+ logger.warning(
+ "Provider call failed, retrying",
+ extra={
+ "operation": description,
+ "attempt": attempt + 1,
+ "max_attempts": max_retries + 1,
+ "delay_seconds": round(delay, 2),
+ "error_code": exc.code,
+ },
+ )
+ await asyncio.sleep(delay)
+
+ raise ProviderError(
+ f"{description} failed after {max_retries + 1} attempts",
+ details={
+ "attempts": max_retries + 1,
+ "last_error": last_error.message if last_error else "unknown",
+ },
+ )
diff --git a/submissions/Victorious/apps/api/app/main.py b/submissions/Victorious/apps/api/app/main.py
new file mode 100644
index 00000000..25707866
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/main.py
@@ -0,0 +1,101 @@
+"""Application entry point.
+
+Assembles the FastAPI application from independently testable pieces. The factory
+takes optional settings so tests can build an app against any configuration
+without touching the environment.
+"""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.api.routers import health as health_router
+from app.api.routers import projects, stream
+from app.core.bootstrap import build_container
+from app.core.config import Settings, get_settings
+from app.core.errors import register_exception_handlers
+from app.core.logging import configure_logging, get_logger
+from app.core.middleware import AccessLogMiddleware, CorrelationMiddleware
+from app.db.session import Database
+
+logger = get_logger(__name__)
+
+
+def create_app(settings: Settings | None = None) -> FastAPI:
+ """Build the application.
+
+ Args:
+ settings: Configuration override. Defaults to the process settings.
+
+ Returns:
+ A fully wired FastAPI application.
+ """
+ resolved = settings or get_settings()
+ configure_logging(resolved.observability)
+
+ @asynccontextmanager
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
+ """Own the container's lifetime.
+
+ Built before the first request and disposed after the last, so database
+ engines and provider clients are released deterministically.
+ """
+ app.state.container = build_container(resolved)
+ logger.info(
+ "%s starting", resolved.app_name,
+ extra={"version": resolved.version, "environment": resolved.environment.value},
+ )
+
+ # Outside production, create any missing tables so a fresh checkout runs
+ # with no setup step. Production schema evolution belongs to Alembic —
+ # create_all cannot express a migration, only an initial shape.
+ if not resolved.is_production:
+ await app.state.container.resolve(Database).create_schema()
+
+ try:
+ yield
+ finally:
+ await app.state.container.aclose()
+ logger.info("%s stopped", resolved.app_name)
+
+ app = FastAPI(
+ title=resolved.app_name,
+ version=resolved.version,
+ description=(
+ "AI-native Software Engineering Organization. Coordinates specialized "
+ "engineering agents across the software lifecycle with shared memory, "
+ "full traceability, and human approval gates."
+ ),
+ docs_url=resolved.docs_url,
+ redoc_url=None,
+ lifespan=lifespan,
+ )
+
+ # Order matters: middleware added last runs first, so correlation IDs are
+ # bound before the access log tries to read one.
+ app.add_middleware(AccessLogMiddleware)
+ app.add_middleware(CorrelationMiddleware)
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=resolved.cors_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ expose_headers=["X-Correlation-ID"],
+ )
+
+ register_exception_handlers(app)
+
+ app.include_router(health_router.router)
+ app.include_router(projects.router, prefix=resolved.api_prefix)
+ app.include_router(projects.approvals_router, prefix=resolved.api_prefix)
+ app.include_router(stream.router, prefix=resolved.api_prefix)
+
+ return app
+
+
+app = create_app()
diff --git a/submissions/Victorious/apps/api/app/memory/__init__.py b/submissions/Victorious/apps/api/app/memory/__init__.py
new file mode 100644
index 00000000..3ab2787a
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/memory/__init__.py
@@ -0,0 +1,42 @@
+"""Shared organizational memory — the single source of truth.
+
+Every engineering agent reads and writes project knowledge through this layer.
+Consumers depend on the protocols in ``repository.py``; the SQL implementation is
+bound in the composition root.
+"""
+
+from app.memory.context_builder import (
+ CHARS_PER_TOKEN,
+ DEFAULT_TOKEN_BUDGET,
+ ContextBuilder,
+ ContextEntry,
+ ProjectContext,
+)
+from app.memory.health import DatabaseHealthCheck
+from app.memory.repository import (
+ AgentRunRepository,
+ ApprovalRepository,
+ ArtifactRepository,
+ EventRepository,
+ ProjectRepository,
+ SharedMemory,
+ TraceRepository,
+)
+from app.memory.sql_repository import SqlSharedMemory
+
+__all__ = [
+ "CHARS_PER_TOKEN",
+ "DEFAULT_TOKEN_BUDGET",
+ "AgentRunRepository",
+ "ApprovalRepository",
+ "ArtifactRepository",
+ "ContextBuilder",
+ "ContextEntry",
+ "DatabaseHealthCheck",
+ "EventRepository",
+ "ProjectContext",
+ "ProjectRepository",
+ "SharedMemory",
+ "SqlSharedMemory",
+ "TraceRepository",
+]
diff --git a/submissions/Victorious/apps/api/app/memory/context_builder.py b/submissions/Victorious/apps/api/app/memory/context_builder.py
new file mode 100644
index 00000000..a631c4c9
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/memory/context_builder.py
@@ -0,0 +1,249 @@
+"""Assembles the project context an agent reasons over.
+
+`02_Proposed_Solution.md` requires "shared context over isolated tasks": each
+stage has access to the decisions and rationale produced by prior stages, so a
+requirement defined once is not restated or reinterpreted downstream.
+
+The builder answers a narrow question — *what should this agent read right now?* —
+and enforces three rules the platform depends on:
+
+1. **Upstream only.** An agent sees stages before its own. Feeding it downstream
+ artifacts would let a later stage's guesses contaminate an earlier decision.
+2. **Approved first.** Approved artifacts are included before drafts, so an agent
+ reasons over what a human has sanctioned rather than over unreviewed output.
+3. **Budgeted.** Context is truncated to a token budget, newest and most relevant
+ retained. `12_Risk_Analysis.md` rates High Token Consumption a Medium risk;
+ an unbounded context window is how that risk materialises.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from app.core.logging import get_logger
+from app.domain.artifacts import Artifact, ArtifactStatus, ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage, preceding_stages
+from app.memory.repository import ArtifactRepository, ProjectRepository
+
+logger = get_logger(__name__)
+
+#: Characters per token. A deliberate approximation — exact counting is
+#: provider-specific and would couple this module to a vendor tokenizer, which
+#: ADR-0004's provider abstraction exists to prevent. Conservative by design:
+#: over-estimating tokens truncates early, which is the safe direction.
+CHARS_PER_TOKEN = 4
+
+DEFAULT_TOKEN_BUDGET = 24_000
+
+
+@dataclass(frozen=True)
+class ContextEntry:
+ """One artifact included in an agent's context."""
+
+ artifact: Artifact
+ body_markdown: str
+ version: int
+ included_fully: bool = True
+
+ @property
+ def estimated_tokens(self) -> int:
+ return len(self.body_markdown) // CHARS_PER_TOKEN
+
+
+@dataclass
+class ProjectContext:
+ """Everything an agent reads before reasoning."""
+
+ project_id: str
+ project_name: str
+ project_description: str
+ stage: LifecycleStage
+ role: AgentRole
+
+ entries: list[ContextEntry] = field(default_factory=list)
+ omitted: list[str] = field(default_factory=list)
+ """Titles of artifacts dropped for budget. Reported rather than hidden, so a
+ thin answer can be explained by what the agent was not shown."""
+
+ @property
+ def estimated_tokens(self) -> int:
+ return sum(entry.estimated_tokens for entry in self.entries)
+
+ @property
+ def artifact_ids(self) -> list[str]:
+ """Inputs recorded on the agent run — the upstream half of a trace edge."""
+ return [entry.artifact.id for entry in self.entries]
+
+ def render(self) -> str:
+ """Render as the markdown block placed in the agent's prompt."""
+ sections = [
+ "# Project context",
+ "",
+ f"**Project:** {self.project_name}",
+ f"**Description:** {self.project_description}",
+ f"**Current stage:** {self.stage.value}",
+ "",
+ ]
+
+ if not self.entries:
+ sections.append("_No upstream artifacts exist yet. This is the first stage._")
+ return "\n".join(sections)
+
+ sections.append("## Upstream engineering artifacts")
+ sections.append("")
+
+ for entry in self.entries:
+ marker = "" if entry.included_fully else " _(truncated)_"
+ sections.extend(
+ [
+ f"### {entry.artifact.title}{marker}",
+ # The artifact ID is emitted deliberately and prominently:
+ # the shared system prompt requires every output to declare
+ # the upstream it derived from using these exact IDs, and the
+ # agent base class rejects artifacts that cite anything it was
+ # not shown. Omitting them here would make that contract
+ # impossible to satisfy.
+ f"- **Artifact ID:** `{entry.artifact.id}`",
+ f"- Type: {entry.artifact.type.value} · "
+ f"Stage: {entry.artifact.stage.value} · "
+ f"Version: {entry.version} · "
+ f"Status: {entry.artifact.status.value}",
+ "",
+ entry.body_markdown,
+ "",
+ ]
+ )
+
+ if self.omitted:
+ sections.extend(
+ [
+ "## Omitted for context budget",
+ "",
+ *(f"- {title}" for title in self.omitted),
+ "",
+ ]
+ )
+
+ return "\n".join(sections)
+
+
+class ContextBuilder:
+ """Builds stage-scoped, budgeted context from shared memory."""
+
+ def __init__(
+ self,
+ projects: ProjectRepository,
+ artifacts: ArtifactRepository,
+ *,
+ token_budget: int = DEFAULT_TOKEN_BUDGET,
+ ) -> None:
+ self._projects = projects
+ self._artifacts = artifacts
+ self._token_budget = token_budget
+
+ async def build(
+ self,
+ project_id: str,
+ *,
+ stage: LifecycleStage,
+ role: AgentRole,
+ include_types: set[ArtifactType] | None = None,
+ ) -> ProjectContext:
+ """Assemble the context for an agent about to work on ``stage``.
+
+ Args:
+ project_id: Project being worked on.
+ stage: Stage the agent is performing. Only earlier stages are visible.
+ role: Agent role, recorded on the context for traceability.
+ include_types: Restrict to specific artifact types. Used when an agent
+ needs a focused view — the QA agent wants acceptance criteria, not
+ the entire architecture.
+
+ Returns:
+ Context within the token budget, with omissions listed.
+ """
+ project = await self._projects.get(project_id)
+ upstream_stages = set(preceding_stages(stage))
+
+ candidates = [
+ artifact
+ for artifact in await self._artifacts.list_for_project(project_id)
+ if artifact.stage in upstream_stages
+ and artifact.has_content
+ and (include_types is None or artifact.type in include_types)
+ ]
+
+ ordered = sorted(candidates, key=self._priority)
+
+ entries: list[ContextEntry] = []
+ omitted: list[str] = []
+ remaining = self._token_budget
+
+ for artifact in ordered:
+ resolved = await self._artifacts.get_version(artifact.id)
+ body = resolved.version.body_markdown
+ cost = len(body) // CHARS_PER_TOKEN
+
+ if cost <= remaining:
+ entries.append(
+ ContextEntry(
+ artifact=artifact,
+ body_markdown=body,
+ version=resolved.version.version,
+ )
+ )
+ remaining -= cost
+ continue
+
+ # Partially include when a meaningful amount still fits. A heading
+ # with two lines under it is worse than nothing: it reads as complete
+ # while being misleading.
+ if remaining > 200:
+ cutoff = remaining * CHARS_PER_TOKEN
+ entries.append(
+ ContextEntry(
+ artifact=artifact,
+ body_markdown=body[:cutoff],
+ version=resolved.version.version,
+ included_fully=False,
+ )
+ )
+ remaining = 0
+ else:
+ omitted.append(artifact.title)
+
+ if omitted:
+ logger.warning(
+ "Context budget exceeded; artifacts omitted",
+ extra={
+ "project_id": project_id,
+ "stage": stage.value,
+ "omitted_count": len(omitted),
+ },
+ )
+
+ return ProjectContext(
+ project_id=project.id,
+ project_name=project.name,
+ project_description=project.description,
+ stage=stage,
+ role=role,
+ entries=entries,
+ omitted=omitted,
+ )
+
+ @staticmethod
+ def _priority(artifact: Artifact) -> tuple[int, float]:
+ """Order candidates by inclusion priority.
+
+ Approved artifacts outrank drafts, and within a status the most recently
+ updated comes first — so when the budget binds, what survives is the
+ sanctioned and current view of the project.
+ """
+ status_rank = {
+ ArtifactStatus.APPROVED: 0,
+ ArtifactStatus.AWAITING_APPROVAL: 1,
+ ArtifactStatus.DRAFT: 2,
+ ArtifactStatus.REJECTED: 3,
+ }
+ return (status_rank[artifact.status], -artifact.updated_at.timestamp())
diff --git a/submissions/Victorious/apps/api/app/memory/health.py b/submissions/Victorious/apps/api/app/memory/health.py
new file mode 100644
index 00000000..004c7afe
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/memory/health.py
@@ -0,0 +1,47 @@
+"""Health check for the shared organizational memory.
+
+Registers into the health registry established in Milestone 0. That registry was
+built to accept checks without modification, and this is the first proof: the
+readiness endpoint and the workspace status panel both pick up the database with
+no change to either.
+"""
+
+from __future__ import annotations
+
+from sqlalchemy import select
+
+from app.core.health import ComponentHealth, HealthStatus
+from app.db.models import ProjectRow
+from app.db.session import Database
+
+
+class DatabaseHealthCheck:
+ """Verifies the shared memory is reachable and its schema is present.
+
+ Deliberately issues a real query against a real table rather than
+ ``SELECT 1``. A connection can be alive while the schema is missing — the
+ exact state after a failed migration — and that must read as unhealthy.
+ """
+
+ def __init__(self, database: Database) -> None:
+ self._db = database
+
+ @property
+ def name(self) -> str:
+ return "shared_memory"
+
+ @property
+ def critical(self) -> bool:
+ """Critical: without memory there is no source of truth to reason over."""
+ return True
+
+ async def check(self) -> ComponentHealth:
+ async with self._db.session() as session:
+ result = await session.execute(select(ProjectRow.id).limit(1))
+ result.first()
+
+ return ComponentHealth(
+ name=self.name,
+ status=HealthStatus.HEALTHY,
+ message="Schema reachable",
+ )
diff --git a/submissions/Victorious/apps/api/app/memory/repository.py b/submissions/Victorious/apps/api/app/memory/repository.py
new file mode 100644
index 00000000..61a92b97
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/memory/repository.py
@@ -0,0 +1,275 @@
+"""Shared organizational memory — persistence protocols.
+
+`15_Development_Guidelines.md`: "Shared memory is the single source of truth...
+Every engineering agent should operate using the same validated project
+knowledge."
+
+Protocols are declared per aggregate so a consumer depends only on what it uses:
+the Documentation Agent needs artifacts, not approvals. :class:`SharedMemory`
+composes them into the single collaborator agents and the orchestrator receive.
+
+No implementation detail appears here — no session, no transaction, no SQL. The
+SQL implementation lives in ``sql_repository.py`` and is bound in the composition
+root, so swapping the backing store touches one file.
+"""
+
+from __future__ import annotations
+
+from typing import Protocol, runtime_checkable
+
+from app.domain.agents import AgentRun
+from app.domain.approvals import ApprovalRequest
+from app.domain.artifacts import (
+ Artifact,
+ ArtifactType,
+ ArtifactVersion,
+ ArtifactWithVersion,
+)
+from app.domain.events import ProjectEvent
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.projects import Project
+from app.domain.reviews import ArtifactReview
+from app.domain.traceability import ImpactAnalysis, StaleEdge, TraceEdge
+
+
+@runtime_checkable
+class ProjectRepository(Protocol):
+ """Projects and their lifecycle state."""
+
+ async def create(self, project: Project) -> Project: ...
+
+ async def get(self, project_id: str) -> Project:
+ """Return a project.
+
+ Raises:
+ NotFoundError: if no such project exists.
+ """
+ ...
+
+ async def list_all(self, *, limit: int = 50, offset: int = 0) -> list[Project]: ...
+
+ async def update(self, project: Project) -> Project: ...
+
+ async def exists(self, project_id: str) -> bool: ...
+
+
+@runtime_checkable
+class ArtifactRepository(Protocol):
+ """Artifacts and their append-only version history."""
+
+ async def create(self, artifact: Artifact) -> Artifact: ...
+
+ async def get(self, artifact_id: str) -> Artifact:
+ """Return artifact identity without content.
+
+ Raises:
+ NotFoundError: if no such artifact exists.
+ """
+ ...
+
+ async def update(self, artifact: Artifact) -> Artifact:
+ """Persist status changes. Never mutates version content."""
+ ...
+
+ async def list_for_project(
+ self,
+ project_id: str,
+ *,
+ stage: LifecycleStage | None = None,
+ artifact_type: ArtifactType | None = None,
+ ) -> list[Artifact]: ...
+
+ async def find_by_identity(
+ self, project_id: str, artifact_type: ArtifactType, stage: LifecycleStage, title: str
+ ) -> Artifact | None:
+ """Find an artifact an agent has produced before.
+
+ Identity is (project, type, stage, title). Title is part of the key
+ because a stage can legitimately produce several artifacts of one type —
+ the Full Stack Engineer writes many source files — while still producing
+ exactly one of each *named* artifact.
+
+ Used when an agent re-runs after a rejection: the revised work becomes a
+ new *version* of what it produced before rather than a second competing
+ artifact, which is what keeps the traceability graph pointing at one
+ stable identity across revisions (ADR-0007).
+ """
+ ...
+
+ async def append_version(
+ self, artifact_id: str, version: ArtifactVersion
+ ) -> ArtifactVersion:
+ """Append a new version and advance the artifact's current version.
+
+ The only way content enters memory. Version numbers are assigned by the
+ repository, not the caller, so two concurrent writers cannot mint the
+ same number.
+
+ Raises:
+ NotFoundError: if the artifact does not exist.
+ """
+ ...
+
+ async def get_version(
+ self, artifact_id: str, version: int | None = None
+ ) -> ArtifactWithVersion:
+ """Return an artifact with one version — the latest when unspecified.
+
+ Raises:
+ NotFoundError: if the artifact or the requested version is absent.
+ """
+ ...
+
+ async def list_versions(self, artifact_id: str) -> list[ArtifactVersion]:
+ """Return every version, oldest first."""
+ ...
+
+ async def current_versions(self, project_id: str) -> dict[str, int]:
+ """Return artifact ID to current version for a whole project.
+
+ The single query that makes staleness computable across the project in
+ one pass rather than N.
+ """
+ ...
+
+
+@runtime_checkable
+class TraceRepository(Protocol):
+ """The traceability graph."""
+
+ async def add_edge(self, edge: TraceEdge) -> TraceEdge: ...
+
+ async def list_for_project(self, project_id: str) -> list[TraceEdge]: ...
+
+ async def upstream_of(self, artifact_id: str) -> list[TraceEdge]:
+ """Return edges this artifact depends on — "why does this exist?"."""
+ ...
+
+ async def downstream_of(self, artifact_id: str) -> list[TraceEdge]:
+ """Return edges depending on this artifact — one hop of blast radius."""
+ ...
+
+ async def analyse_impact(
+ self, project_id: str, artifact_id: str, *, max_depth: int | None = None
+ ) -> ImpactAnalysis:
+ """Compute the transitive downstream impact of changing an artifact."""
+ ...
+
+ async def stale_edges(self, project_id: str) -> list[StaleEdge]:
+ """Return derivations whose upstream has advanced past the cited version."""
+ ...
+
+
+@runtime_checkable
+class AgentRunRepository(Protocol):
+ """Agent execution records."""
+
+ async def create(self, run: AgentRun) -> AgentRun: ...
+
+ async def get(self, run_id: str) -> AgentRun: ...
+
+ async def update(self, run: AgentRun) -> AgentRun: ...
+
+ async def list_for_project(
+ self, project_id: str, *, role: AgentRole | None = None, limit: int = 100
+ ) -> list[AgentRun]: ...
+
+ async def latest_for_role(self, project_id: str, role: AgentRole) -> AgentRun | None:
+ """Return the most recent run for a role.
+
+ Drives the Agent Organization view, which shows each agent's current
+ state whether or not it is running right now.
+ """
+ ...
+
+
+@runtime_checkable
+class ApprovalRepository(Protocol):
+ """Human approval gates."""
+
+ async def create(self, request: ApprovalRequest) -> ApprovalRequest: ...
+
+ async def get(self, approval_id: str) -> ApprovalRequest: ...
+
+ async def update(self, request: ApprovalRequest) -> ApprovalRequest: ...
+
+ async def list_for_project(
+ self, project_id: str, *, pending_only: bool = False
+ ) -> list[ApprovalRequest]: ...
+
+ async def list_pending(self, *, limit: int = 50) -> list[ApprovalRequest]:
+ """Return pending approvals across all projects, for the dashboard."""
+ ...
+
+
+@runtime_checkable
+class EventRepository(Protocol):
+ """Append-only engineering activity record."""
+
+ async def append(self, event: ProjectEvent) -> ProjectEvent: ...
+
+ async def list_for_project(
+ self, project_id: str, *, limit: int = 200, after_id: str | None = None
+ ) -> list[ProjectEvent]:
+ """Return events oldest first.
+
+ ``after_id`` supports stream resumption: a browser that reconnects to the
+ live agent feed replays only what it missed.
+ """
+ ...
+
+ async def list_recent(self, *, limit: int = 50) -> list[ProjectEvent]:
+ """Return recent events across all projects, for the dashboard."""
+ ...
+
+
+@runtime_checkable
+class ReviewRepository(Protocol):
+ """Engineering reviews, keyed to an artifact version."""
+
+ async def upsert(self, review: ArtifactReview) -> ArtifactReview:
+ """Store a review, replacing any earlier review of the same version.
+
+ Upsert rather than append: a review is a judgement *of a version*, and a
+ version is immutable, so a second review of it supersedes the first
+ rather than accumulating alongside it.
+ """
+ ...
+
+ async def list_for_project(self, project_id: str) -> list[ArtifactReview]: ...
+
+ async def for_artifact(
+ self, artifact_id: str, version: int | None = None
+ ) -> ArtifactReview | None:
+ """Return the review of a version, or of the latest reviewed version."""
+ ...
+
+
+class SharedMemory(Protocol):
+ """The single source of truth, as one injectable collaborator.
+
+ Agents and the orchestrator depend on this rather than on seven separate
+ repositories, which keeps their signatures honest: an agent that can read the
+ project can read all of it.
+ """
+
+ @property
+ def projects(self) -> ProjectRepository: ...
+
+ @property
+ def artifacts(self) -> ArtifactRepository: ...
+
+ @property
+ def traces(self) -> TraceRepository: ...
+
+ @property
+ def runs(self) -> AgentRunRepository: ...
+
+ @property
+ def approvals(self) -> ApprovalRepository: ...
+
+ @property
+ def events(self) -> EventRepository: ...
+
+ @property
+ def reviews(self) -> ReviewRepository: ...
diff --git a/submissions/Victorious/apps/api/app/memory/sql_repository.py b/submissions/Victorious/apps/api/app/memory/sql_repository.py
new file mode 100644
index 00000000..3a2948fe
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/memory/sql_repository.py
@@ -0,0 +1,838 @@
+"""SQL-backed shared organizational memory.
+
+Implements every protocol in ``repository.py`` over SQLAlchemy. Mapping between
+storage rows and domain models happens here and nowhere else, which is what keeps
+the domain layer framework-free.
+
+Each public method opens its own transaction. Multi-step engineering operations
+that must be atomic — appending a version *and* recording its trace edges — are
+expressed as single methods rather than as several calls the caller must
+sequence, so a caller cannot leave memory half-written.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from datetime import UTC, datetime
+from typing import Any
+
+from sqlalchemy import func, select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.core.logging import get_logger
+from app.db.models import (
+ AgentRunRow,
+ ApprovalRow,
+ ArtifactReviewRow,
+ ArtifactRow,
+ ArtifactVersionRow,
+ EventRow,
+ ProjectRow,
+ TraceEdgeRow,
+)
+from app.db.session import Database
+from app.domain.agents import AgentRun, AgentRunStatus, TokenUsage
+from app.domain.approvals import ApprovalKind, ApprovalRequest, ApprovalStatus
+from app.domain.artifacts import (
+ Artifact,
+ ArtifactStatus,
+ ArtifactType,
+ ArtifactVersion,
+ ArtifactWithVersion,
+)
+from app.domain.errors import ConflictError, NotFoundError
+from app.domain.events import EventType, ProjectEvent
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.projects import Project, StageState
+from app.domain.reviews import ArtifactReview, ReviewFinding, ReviewVerdict
+from app.domain.traceability import (
+ ImpactAnalysis,
+ StaleEdge,
+ TraceEdge,
+ TraceKind,
+ analyse_impact,
+ stale_edges,
+)
+
+logger = get_logger(__name__)
+
+
+# --- Mapping -----------------------------------------------------------------
+# Row-to-domain conversion lives in module functions rather than on the rows
+# themselves, so the SQLAlchemy models stay free of domain knowledge and the
+# dependency continues to point one way only.
+
+
+def _to_project(row: ProjectRow) -> Project:
+ return Project(
+ id=row.id,
+ name=row.name,
+ description=row.description,
+ current_stage=LifecycleStage(row.current_stage),
+ stages=[StageState.model_validate(state) for state in row.stages],
+ created_at=row.created_at,
+ updated_at=row.updated_at,
+ )
+
+
+def _to_artifact(row: ArtifactRow) -> Artifact:
+ return Artifact(
+ id=row.id,
+ project_id=row.project_id,
+ type=ArtifactType(row.type),
+ title=row.title,
+ stage=LifecycleStage(row.stage),
+ owner_role=AgentRole(row.owner_role),
+ status=ArtifactStatus(row.status),
+ current_version=row.current_version,
+ created_at=row.created_at,
+ updated_at=row.updated_at,
+ )
+
+
+def _to_version(row: ArtifactVersionRow) -> ArtifactVersion:
+ return ArtifactVersion(
+ id=row.id,
+ artifact_id=row.artifact_id,
+ version=row.version,
+ body_markdown=row.body_markdown,
+ content=row.content,
+ produced_by_run_id=row.produced_by_run_id,
+ summary=row.summary,
+ confidence=row.confidence,
+ created_at=row.created_at,
+ )
+
+
+def _to_edge(row: TraceEdgeRow) -> TraceEdge:
+ return TraceEdge(
+ id=row.id,
+ project_id=row.project_id,
+ upstream_artifact_id=row.upstream_artifact_id,
+ downstream_artifact_id=row.downstream_artifact_id,
+ kind=TraceKind(row.kind),
+ upstream_version=row.upstream_version,
+ created_by_run_id=row.created_by_run_id,
+ rationale=row.rationale,
+ created_at=row.created_at,
+ )
+
+
+def _to_run(row: AgentRunRow) -> AgentRun:
+ return AgentRun(
+ id=row.id,
+ project_id=row.project_id,
+ role=AgentRole(row.role),
+ stage=LifecycleStage(row.stage),
+ status=AgentRunStatus(row.status),
+ task=row.task,
+ reasoning_summary=row.reasoning_summary,
+ confidence=row.confidence,
+ input_artifact_ids=list(row.input_artifact_ids),
+ output_artifact_ids=list(row.output_artifact_ids),
+ blocked_on=list(row.blocked_on),
+ provider=row.provider,
+ model=row.model,
+ token_usage=TokenUsage(
+ input_tokens=row.input_tokens, output_tokens=row.output_tokens
+ ),
+ requires_approval=row.requires_approval,
+ approval_reason=row.approval_reason,
+ correlation_id=row.correlation_id,
+ error=row.error,
+ started_at=row.started_at,
+ completed_at=row.completed_at,
+ )
+
+
+def _to_approval(row: ApprovalRow) -> ApprovalRequest:
+ return ApprovalRequest(
+ id=row.id,
+ project_id=row.project_id,
+ kind=ApprovalKind(row.kind),
+ stage=LifecycleStage(row.stage),
+ title=row.title,
+ what_changed=row.what_changed,
+ why=row.why,
+ requested_by=AgentRole(row.requested_by),
+ agents_involved=[AgentRole(role) for role in row.agents_involved],
+ artifact_ids=list(row.artifact_ids),
+ impact=ImpactAnalysis.model_validate(row.impact) if row.impact else None,
+ status=ApprovalStatus(row.status),
+ feedback=row.feedback,
+ decided_at=row.decided_at,
+ created_at=row.created_at,
+ )
+
+
+def _to_review(row: ArtifactReviewRow) -> ArtifactReview:
+ return ArtifactReview(
+ id=row.id,
+ project_id=row.project_id,
+ artifact_id=row.artifact_id,
+ artifact_version=row.artifact_version,
+ stage=LifecycleStage(row.stage),
+ role=AgentRole(row.role),
+ produced_by_run_id=row.produced_by_run_id,
+ quality_score=row.quality_score,
+ verdict=ReviewVerdict(row.verdict),
+ summary=row.summary,
+ strengths=[ReviewFinding.model_validate(item) for item in row.strengths],
+ weaknesses=[ReviewFinding.model_validate(item) for item in row.weaknesses],
+ suggestions=[ReviewFinding.model_validate(item) for item in row.suggestions],
+ deterministic_score=row.deterministic_score,
+ reasoning_applied=row.reasoning_applied,
+ reviewer_provider=row.reviewer_provider,
+ reviewer_model=row.reviewer_model,
+ created_at=row.created_at,
+ )
+
+
+def _to_event(row: EventRow) -> ProjectEvent:
+ return ProjectEvent(
+ id=row.id,
+ project_id=row.project_id,
+ type=EventType(row.type),
+ stage=LifecycleStage(row.stage) if row.stage else None,
+ role=AgentRole(row.role) if row.role else None,
+ summary=row.summary,
+ payload=row.payload,
+ correlation_id=row.correlation_id,
+ created_at=row.created_at,
+ )
+
+
+# --- Repositories -------------------------------------------------------------
+
+
+class _Base:
+ """Shared session access for every repository."""
+
+ def __init__(self, database: Database) -> None:
+ self._db = database
+
+
+class SqlProjectRepository(_Base):
+ async def create(self, project: Project) -> Project:
+ async with self._db.session() as session:
+ session.add(
+ ProjectRow(
+ id=project.id,
+ name=project.name,
+ description=project.description,
+ current_stage=project.current_stage.value,
+ stages=[state.model_dump(mode="json") for state in project.stages],
+ created_at=project.created_at,
+ updated_at=project.updated_at,
+ )
+ )
+ logger.info("Project created", extra={"project_id": project.id})
+ return project
+
+ async def get(self, project_id: str) -> Project:
+ async with self._db.session() as session:
+ row = await session.get(ProjectRow, project_id)
+ if row is None:
+ raise NotFoundError("Project not found", details={"project_id": project_id})
+ return _to_project(row)
+
+ async def list_all(self, *, limit: int = 50, offset: int = 0) -> list[Project]:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(ProjectRow)
+ .order_by(ProjectRow.updated_at.desc())
+ .limit(limit)
+ .offset(offset)
+ )
+ return [_to_project(row) for row in result.scalars()]
+
+ async def update(self, project: Project) -> Project:
+ async with self._db.session() as session:
+ row = await session.get(ProjectRow, project.id)
+ if row is None:
+ raise NotFoundError("Project not found", details={"project_id": project.id})
+
+ row.name = project.name
+ row.description = project.description
+ row.current_stage = project.current_stage.value
+ row.stages = [state.model_dump(mode="json") for state in project.stages]
+ row.updated_at = datetime.now(UTC)
+
+ return _to_project(row)
+
+ async def exists(self, project_id: str) -> bool:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(func.count()).select_from(ProjectRow).where(ProjectRow.id == project_id)
+ )
+ return (result.scalar_one() or 0) > 0
+
+
+class SqlArtifactRepository(_Base):
+ async def create(self, artifact: Artifact) -> Artifact:
+ async with self._db.session() as session:
+ session.add(
+ ArtifactRow(
+ id=artifact.id,
+ project_id=artifact.project_id,
+ type=artifact.type.value,
+ title=artifact.title,
+ stage=artifact.stage.value,
+ owner_role=artifact.owner_role.value,
+ status=artifact.status.value,
+ current_version=artifact.current_version,
+ created_at=artifact.created_at,
+ updated_at=artifact.updated_at,
+ )
+ )
+ return artifact
+
+ async def get(self, artifact_id: str) -> Artifact:
+ async with self._db.session() as session:
+ row = await self._require(session, artifact_id)
+ return _to_artifact(row)
+
+ async def update(self, artifact: Artifact) -> Artifact:
+ async with self._db.session() as session:
+ row = await self._require(session, artifact.id)
+ row.title = artifact.title
+ row.status = artifact.status.value
+ row.updated_at = datetime.now(UTC)
+ return _to_artifact(row)
+
+ async def list_for_project(
+ self,
+ project_id: str,
+ *,
+ stage: LifecycleStage | None = None,
+ artifact_type: ArtifactType | None = None,
+ ) -> list[Artifact]:
+ async with self._db.session() as session:
+ query = select(ArtifactRow).where(ArtifactRow.project_id == project_id)
+ if stage is not None:
+ query = query.where(ArtifactRow.stage == stage.value)
+ if artifact_type is not None:
+ query = query.where(ArtifactRow.type == artifact_type.value)
+
+ result = await session.execute(query.order_by(ArtifactRow.created_at))
+ return [_to_artifact(row) for row in result.scalars()]
+
+ async def find_by_identity(
+ self, project_id: str, artifact_type: ArtifactType, stage: LifecycleStage, title: str
+ ) -> Artifact | None:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(ArtifactRow).where(
+ ArtifactRow.project_id == project_id,
+ ArtifactRow.type == artifact_type.value,
+ ArtifactRow.stage == stage.value,
+ ArtifactRow.title == title,
+ )
+ )
+ row = result.scalars().first()
+ return _to_artifact(row) if row else None
+
+ async def append_version(
+ self, artifact_id: str, version: ArtifactVersion
+ ) -> ArtifactVersion:
+ """Append a version and advance the artifact's current version.
+
+ The version number is assigned here from the artifact's current value,
+ ignoring any number the caller supplied. Two writers racing would
+ otherwise both compute the same next number; the unique constraint on
+ ``(artifact_id, version)`` turns that into a database error rather than
+ silent overwriting, which is surfaced as a ``ConflictError``.
+ """
+ async with self._db.session() as session:
+ artifact_row = await self._require(session, artifact_id)
+ next_version = artifact_row.current_version + 1
+
+ stored = ArtifactVersion(
+ id=version.id,
+ artifact_id=artifact_id,
+ version=next_version,
+ body_markdown=version.body_markdown,
+ content=version.content,
+ produced_by_run_id=version.produced_by_run_id,
+ summary=version.summary,
+ confidence=version.confidence,
+ created_at=version.created_at,
+ )
+
+ session.add(
+ ArtifactVersionRow(
+ id=stored.id,
+ artifact_id=artifact_id,
+ version=stored.version,
+ body_markdown=stored.body_markdown,
+ content=stored.content,
+ produced_by_run_id=stored.produced_by_run_id,
+ summary=stored.summary,
+ confidence=stored.confidence,
+ created_at=stored.created_at,
+ )
+ )
+
+ artifact_row.current_version = next_version
+ artifact_row.updated_at = datetime.now(UTC)
+
+ try:
+ await session.flush()
+ except Exception as exc:
+ raise ConflictError(
+ "Artifact version was written concurrently",
+ details={"artifact_id": artifact_id, "version": next_version},
+ ) from exc
+
+ logger.info(
+ "Artifact version appended",
+ extra={"artifact_id": artifact_id, "version": next_version},
+ )
+ return stored
+
+ async def get_version(
+ self, artifact_id: str, version: int | None = None
+ ) -> ArtifactWithVersion:
+ async with self._db.session() as session:
+ artifact_row = await self._require(session, artifact_id)
+
+ target = version if version is not None else artifact_row.current_version
+ if target < 1:
+ raise NotFoundError(
+ "Artifact has no versions yet", details={"artifact_id": artifact_id}
+ )
+
+ result = await session.execute(
+ select(ArtifactVersionRow).where(
+ ArtifactVersionRow.artifact_id == artifact_id,
+ ArtifactVersionRow.version == target,
+ )
+ )
+ version_row = result.scalar_one_or_none()
+ if version_row is None:
+ raise NotFoundError(
+ "Artifact version not found",
+ details={"artifact_id": artifact_id, "version": target},
+ )
+
+ return ArtifactWithVersion(
+ artifact=_to_artifact(artifact_row), version=_to_version(version_row)
+ )
+
+ async def list_versions(self, artifact_id: str) -> list[ArtifactVersion]:
+ async with self._db.session() as session:
+ await self._require(session, artifact_id)
+ result = await session.execute(
+ select(ArtifactVersionRow)
+ .where(ArtifactVersionRow.artifact_id == artifact_id)
+ .order_by(ArtifactVersionRow.version)
+ )
+ return [_to_version(row) for row in result.scalars()]
+
+ async def current_versions(self, project_id: str) -> dict[str, int]:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(ArtifactRow.id, ArtifactRow.current_version).where(
+ ArtifactRow.project_id == project_id
+ )
+ )
+ return dict(result.all()) # type: ignore[arg-type]
+
+ @staticmethod
+ async def _require(session: AsyncSession, artifact_id: str) -> ArtifactRow:
+ row = await session.get(ArtifactRow, artifact_id)
+ if row is None:
+ raise NotFoundError("Artifact not found", details={"artifact_id": artifact_id})
+ return row
+
+
+class SqlTraceRepository(_Base):
+ async def add_edge(self, edge: TraceEdge) -> TraceEdge:
+ async with self._db.session() as session:
+ session.add(
+ TraceEdgeRow(
+ id=edge.id,
+ project_id=edge.project_id,
+ upstream_artifact_id=edge.upstream_artifact_id,
+ downstream_artifact_id=edge.downstream_artifact_id,
+ kind=edge.kind.value,
+ upstream_version=edge.upstream_version,
+ created_by_run_id=edge.created_by_run_id,
+ rationale=edge.rationale,
+ created_at=edge.created_at,
+ )
+ )
+ return edge
+
+ async def list_for_project(self, project_id: str) -> list[TraceEdge]:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(TraceEdgeRow)
+ .where(TraceEdgeRow.project_id == project_id)
+ .order_by(TraceEdgeRow.created_at)
+ )
+ return [_to_edge(row) for row in result.scalars()]
+
+ async def upstream_of(self, artifact_id: str) -> list[TraceEdge]:
+ return await self._by_direction(TraceEdgeRow.downstream_artifact_id, artifact_id)
+
+ async def downstream_of(self, artifact_id: str) -> list[TraceEdge]:
+ return await self._by_direction(TraceEdgeRow.upstream_artifact_id, artifact_id)
+
+ async def analyse_impact(
+ self, project_id: str, artifact_id: str, *, max_depth: int | None = None
+ ) -> ImpactAnalysis:
+ """Compute blast radius.
+
+ The whole project's edges are loaded and traversed in memory rather than
+ walked with recursive SQL. At MVP scale — hundreds of edges — this is
+ faster than N round trips, and it keeps the traversal rules in the pure,
+ directly testable domain function.
+ """
+ edges = await self.list_for_project(project_id)
+ return analyse_impact(artifact_id, edges, max_depth=max_depth)
+
+ async def stale_edges(self, project_id: str) -> list[StaleEdge]:
+ async with self._db.session() as session:
+ edge_result = await session.execute(
+ select(TraceEdgeRow).where(TraceEdgeRow.project_id == project_id)
+ )
+ edges = [_to_edge(row) for row in edge_result.scalars()]
+
+ version_result = await session.execute(
+ select(ArtifactRow.id, ArtifactRow.current_version).where(
+ ArtifactRow.project_id == project_id
+ )
+ )
+ current: dict[str, int] = dict(version_result.all()) # type: ignore[arg-type]
+
+ return stale_edges(edges, current)
+
+ async def _by_direction(self, column: Any, artifact_id: str) -> list[TraceEdge]:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(TraceEdgeRow).where(column == artifact_id).order_by(TraceEdgeRow.created_at)
+ )
+ return [_to_edge(row) for row in result.scalars()]
+
+
+class SqlAgentRunRepository(_Base):
+ async def create(self, run: AgentRun) -> AgentRun:
+ async with self._db.session() as session:
+ session.add(self._to_row(run))
+ return run
+
+ async def get(self, run_id: str) -> AgentRun:
+ async with self._db.session() as session:
+ row = await session.get(AgentRunRow, run_id)
+ if row is None:
+ raise NotFoundError("Agent run not found", details={"run_id": run_id})
+ return _to_run(row)
+
+ async def update(self, run: AgentRun) -> AgentRun:
+ async with self._db.session() as session:
+ row = await session.get(AgentRunRow, run.id)
+ if row is None:
+ raise NotFoundError("Agent run not found", details={"run_id": run.id})
+
+ row.status = run.status.value
+ row.task = run.task
+ row.reasoning_summary = run.reasoning_summary
+ row.confidence = run.confidence
+ row.input_artifact_ids = list(run.input_artifact_ids)
+ row.output_artifact_ids = list(run.output_artifact_ids)
+ row.blocked_on = list(run.blocked_on)
+ row.provider = run.provider
+ row.model = run.model
+ row.input_tokens = run.token_usage.input_tokens
+ row.output_tokens = run.token_usage.output_tokens
+ row.requires_approval = run.requires_approval
+ row.approval_reason = run.approval_reason
+ row.error = run.error
+ row.completed_at = run.completed_at
+
+ return _to_run(row)
+
+ async def list_for_project(
+ self, project_id: str, *, role: AgentRole | None = None, limit: int = 100
+ ) -> list[AgentRun]:
+ async with self._db.session() as session:
+ query = select(AgentRunRow).where(AgentRunRow.project_id == project_id)
+ if role is not None:
+ query = query.where(AgentRunRow.role == role.value)
+
+ result = await session.execute(
+ query.order_by(AgentRunRow.started_at.desc()).limit(limit)
+ )
+ return [_to_run(row) for row in result.scalars()]
+
+ async def latest_for_role(self, project_id: str, role: AgentRole) -> AgentRun | None:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(AgentRunRow)
+ .where(
+ AgentRunRow.project_id == project_id,
+ AgentRunRow.role == role.value,
+ )
+ .order_by(AgentRunRow.started_at.desc())
+ .limit(1)
+ )
+ row = result.scalar_one_or_none()
+ return _to_run(row) if row else None
+
+ @staticmethod
+ def _to_row(run: AgentRun) -> AgentRunRow:
+ return AgentRunRow(
+ id=run.id,
+ project_id=run.project_id,
+ role=run.role.value,
+ stage=run.stage.value,
+ status=run.status.value,
+ task=run.task,
+ reasoning_summary=run.reasoning_summary,
+ confidence=run.confidence,
+ input_artifact_ids=list(run.input_artifact_ids),
+ output_artifact_ids=list(run.output_artifact_ids),
+ blocked_on=list(run.blocked_on),
+ provider=run.provider,
+ model=run.model,
+ input_tokens=run.token_usage.input_tokens,
+ output_tokens=run.token_usage.output_tokens,
+ requires_approval=run.requires_approval,
+ approval_reason=run.approval_reason,
+ correlation_id=run.correlation_id,
+ error=run.error,
+ started_at=run.started_at,
+ completed_at=run.completed_at,
+ )
+
+
+class SqlApprovalRepository(_Base):
+ async def create(self, request: ApprovalRequest) -> ApprovalRequest:
+ async with self._db.session() as session:
+ session.add(
+ ApprovalRow(
+ id=request.id,
+ project_id=request.project_id,
+ kind=request.kind.value,
+ stage=request.stage.value,
+ title=request.title,
+ what_changed=request.what_changed,
+ why=request.why,
+ requested_by=request.requested_by.value,
+ agents_involved=[role.value for role in request.agents_involved],
+ artifact_ids=list(request.artifact_ids),
+ impact=request.impact.model_dump(mode="json") if request.impact else None,
+ status=request.status.value,
+ feedback=request.feedback,
+ decided_at=request.decided_at,
+ created_at=request.created_at,
+ )
+ )
+ return request
+
+ async def get(self, approval_id: str) -> ApprovalRequest:
+ async with self._db.session() as session:
+ row = await session.get(ApprovalRow, approval_id)
+ if row is None:
+ raise NotFoundError(
+ "Approval request not found", details={"approval_id": approval_id}
+ )
+ return _to_approval(row)
+
+ async def update(self, request: ApprovalRequest) -> ApprovalRequest:
+ async with self._db.session() as session:
+ row = await session.get(ApprovalRow, request.id)
+ if row is None:
+ raise NotFoundError(
+ "Approval request not found", details={"approval_id": request.id}
+ )
+
+ row.status = request.status.value
+ row.feedback = request.feedback
+ row.decided_at = request.decided_at
+
+ return _to_approval(row)
+
+ async def list_for_project(
+ self, project_id: str, *, pending_only: bool = False
+ ) -> list[ApprovalRequest]:
+ async with self._db.session() as session:
+ query = select(ApprovalRow).where(ApprovalRow.project_id == project_id)
+ if pending_only:
+ query = query.where(ApprovalRow.status == ApprovalStatus.PENDING.value)
+
+ result = await session.execute(query.order_by(ApprovalRow.created_at.desc()))
+ return [_to_approval(row) for row in result.scalars()]
+
+ async def list_pending(self, *, limit: int = 50) -> list[ApprovalRequest]:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(ApprovalRow)
+ .where(ApprovalRow.status == ApprovalStatus.PENDING.value)
+ .order_by(ApprovalRow.created_at)
+ .limit(limit)
+ )
+ return [_to_approval(row) for row in result.scalars()]
+
+
+class SqlEventRepository(_Base):
+ async def append(self, event: ProjectEvent) -> ProjectEvent:
+ async with self._db.session() as session:
+ session.add(
+ EventRow(
+ id=event.id,
+ project_id=event.project_id,
+ type=event.type.value,
+ stage=event.stage.value if event.stage else None,
+ role=event.role.value if event.role else None,
+ summary=event.summary,
+ payload=event.payload,
+ correlation_id=event.correlation_id,
+ created_at=event.created_at,
+ )
+ )
+ return event
+
+ async def list_for_project(
+ self, project_id: str, *, limit: int = 200, after_id: str | None = None
+ ) -> list[ProjectEvent]:
+ async with self._db.session() as session:
+ query = select(EventRow).where(EventRow.project_id == project_id)
+
+ if after_id is not None:
+ # Resolve the cursor's sequence number, then take everything
+ # after it. An unknown cursor replays from the start rather than
+ # returning nothing, so a stale browser reconnect self-heals.
+ cursor = await session.execute(
+ select(EventRow.seq).where(EventRow.id == after_id)
+ )
+ seq = cursor.scalar_one_or_none()
+ if seq is not None:
+ query = query.where(EventRow.seq > seq)
+
+ result = await session.execute(query.order_by(EventRow.seq).limit(limit))
+ return [_to_event(row) for row in result.scalars()]
+
+ async def list_recent(self, *, limit: int = 50) -> list[ProjectEvent]:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(EventRow).order_by(EventRow.seq.desc()).limit(limit)
+ )
+ rows: Sequence[EventRow] = list(result.scalars())
+ return [_to_event(row) for row in reversed(rows)]
+
+
+class SqlReviewRepository(_Base):
+ async def upsert(self, review: ArtifactReview) -> ArtifactReview:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(ArtifactReviewRow).where(
+ ArtifactReviewRow.artifact_id == review.artifact_id,
+ ArtifactReviewRow.artifact_version == review.artifact_version,
+ )
+ )
+ row = result.scalar_one_or_none()
+
+ payload = {
+ "quality_score": review.quality_score,
+ "verdict": review.verdict.value,
+ "summary": review.summary,
+ "strengths": [item.model_dump(mode="json") for item in review.strengths],
+ "weaknesses": [item.model_dump(mode="json") for item in review.weaknesses],
+ "suggestions": [item.model_dump(mode="json") for item in review.suggestions],
+ "deterministic_score": review.deterministic_score,
+ "reasoning_applied": review.reasoning_applied,
+ "reviewer_provider": review.reviewer_provider,
+ "reviewer_model": review.reviewer_model,
+ }
+
+ if row is None:
+ session.add(
+ ArtifactReviewRow(
+ id=review.id,
+ project_id=review.project_id,
+ artifact_id=review.artifact_id,
+ artifact_version=review.artifact_version,
+ stage=review.stage.value,
+ role=review.role.value,
+ produced_by_run_id=review.produced_by_run_id,
+ created_at=review.created_at,
+ **payload,
+ )
+ )
+ else:
+ for key, value in payload.items():
+ setattr(row, key, value)
+
+ return review
+
+ async def list_for_project(self, project_id: str) -> list[ArtifactReview]:
+ async with self._db.session() as session:
+ result = await session.execute(
+ select(ArtifactReviewRow)
+ .where(ArtifactReviewRow.project_id == project_id)
+ .order_by(ArtifactReviewRow.created_at)
+ )
+ return [_to_review(row) for row in result.scalars()]
+
+ async def for_artifact(
+ self, artifact_id: str, version: int | None = None
+ ) -> ArtifactReview | None:
+ async with self._db.session() as session:
+ query = select(ArtifactReviewRow).where(
+ ArtifactReviewRow.artifact_id == artifact_id
+ )
+ if version is not None:
+ query = query.where(ArtifactReviewRow.artifact_version == version)
+
+ result = await session.execute(
+ query.order_by(ArtifactReviewRow.artifact_version.desc()).limit(1)
+ )
+ row = result.scalar_one_or_none()
+ return _to_review(row) if row else None
+
+
+class SqlSharedMemory:
+ """Composes every repository into the single memory collaborator.
+
+ What agents and the orchestrator receive. They never see a session, a query,
+ or a row.
+ """
+
+ def __init__(self, database: Database) -> None:
+ self._projects = SqlProjectRepository(database)
+ self._artifacts = SqlArtifactRepository(database)
+ self._traces = SqlTraceRepository(database)
+ self._runs = SqlAgentRunRepository(database)
+ self._approvals = SqlApprovalRepository(database)
+ self._events = SqlEventRepository(database)
+ self._reviews = SqlReviewRepository(database)
+
+ @property
+ def projects(self) -> SqlProjectRepository:
+ return self._projects
+
+ @property
+ def artifacts(self) -> SqlArtifactRepository:
+ return self._artifacts
+
+ @property
+ def traces(self) -> SqlTraceRepository:
+ return self._traces
+
+ @property
+ def runs(self) -> SqlAgentRunRepository:
+ return self._runs
+
+ @property
+ def approvals(self) -> SqlApprovalRepository:
+ return self._approvals
+
+ @property
+ def events(self) -> SqlEventRepository:
+ return self._events
+
+ @property
+ def reviews(self) -> SqlReviewRepository:
+ return self._reviews
diff --git a/submissions/Victorious/apps/api/app/orchestration/__init__.py b/submissions/Victorious/apps/api/app/orchestration/__init__.py
new file mode 100644
index 00000000..42905545
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/__init__.py
@@ -0,0 +1,56 @@
+"""Engineering workflow orchestration.
+
+The Executive AI (Engineering Director) and the workflow graph it drives. This
+layer coordinates the organization; it performs no engineering work of its own
+(`15_Development_Guidelines.md`).
+"""
+
+from app.orchestration.conflicts import (
+ LOW_CONFIDENCE_THRESHOLD,
+ Conflict,
+ ConflictKind,
+ ConflictSeverity,
+ blocking,
+ detect_conflicts,
+)
+from app.orchestration.dependencies import (
+ STAGE_GATES,
+ STAGE_INPUTS,
+ ProjectSnapshot,
+ Readiness,
+ ReadinessStatus,
+ evaluate_readiness,
+)
+from app.orchestration.dispatcher import AgentDispatcher, RegistryDispatcher
+from app.orchestration.executive import (
+ ApprovalNarration,
+ CoordinationAction,
+ CoordinationDecision,
+ ExecutiveAI,
+)
+from app.orchestration.graph import build_workflow
+from app.orchestration.runner import OrchestrationOutcome, OrchestrationRunner
+
+__all__ = [
+ "LOW_CONFIDENCE_THRESHOLD",
+ "STAGE_GATES",
+ "STAGE_INPUTS",
+ "AgentDispatcher",
+ "ApprovalNarration",
+ "Conflict",
+ "ConflictKind",
+ "ConflictSeverity",
+ "CoordinationAction",
+ "CoordinationDecision",
+ "ExecutiveAI",
+ "OrchestrationOutcome",
+ "OrchestrationRunner",
+ "ProjectSnapshot",
+ "Readiness",
+ "ReadinessStatus",
+ "RegistryDispatcher",
+ "blocking",
+ "build_workflow",
+ "detect_conflicts",
+ "evaluate_readiness",
+]
diff --git a/submissions/Victorious/apps/api/app/orchestration/conflicts.py b/submissions/Victorious/apps/api/app/orchestration/conflicts.py
new file mode 100644
index 00000000..9593656e
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/conflicts.py
@@ -0,0 +1,235 @@
+"""Conflict detection across agent outputs.
+
+`12_Risk_Analysis.md` rates Agent Coordination Failure a High risk — "multiple
+engineering agents may generate conflicting recommendations or inconsistent
+engineering artifacts" — and prescribes conflict detection as a mitigation.
+`02_Proposed_Solution.md` requires each stage to be able to surface a problem
+discovered late "rather than silently working around it".
+
+Every detector here is deterministic. A conflict is a structural fact about the
+artifacts and the traceability graph — an architecture derived from superseded
+requirements, two competing sources of truth for one artifact type, an agent's
+own stated concern left unresolved. None of that requires a language model, and
+using one would put hallucination risk into the mechanism whose entire job is
+catching inconsistency.
+"""
+
+from __future__ import annotations
+
+from collections import defaultdict
+from collections.abc import Iterable, Mapping
+from enum import StrEnum
+
+from pydantic import BaseModel, Field
+
+from app.domain.agents import AgentRun
+from app.domain.artifacts import Artifact, ArtifactStatus
+from app.domain.lifecycle import AgentRole
+from app.domain.traceability import TraceEdge, stale_edges
+
+#: Below this, an agent is signalling that it was not confident in its own work.
+#: `12_Risk_Analysis.md` names confidence scoring as a hallucination mitigation;
+#: the mitigation only bites if something acts on a low score.
+LOW_CONFIDENCE_THRESHOLD = 0.5
+
+
+class ConflictKind(StrEnum):
+ """What kind of inconsistency was found."""
+
+ STALE_DERIVATION = "stale_derivation"
+ """An artifact was derived from a version of its upstream that has moved on."""
+
+ DUPLICATE_AUTHORITY = "duplicate_authority"
+ """Two approved artifacts of the same type — two competing sources of truth."""
+
+ UNRESOLVED_CONCERN = "unresolved_concern"
+ """An agent flagged a problem with upstream work that nothing has addressed."""
+
+ LOW_CONFIDENCE = "low_confidence"
+ """An agent completed work it was not confident in."""
+
+
+class ConflictSeverity(StrEnum):
+ """How a conflict should affect the workflow."""
+
+ BLOCKING = "blocking"
+ """Progress must stop; proceeding would build on a known inconsistency."""
+
+ ADVISORY = "advisory"
+ """Worth a human's attention, but not a reason to halt."""
+
+
+class Conflict(BaseModel):
+ """One detected inconsistency."""
+
+ kind: ConflictKind
+ severity: ConflictSeverity
+ summary: str = Field(description="One line, rendered directly in the workspace.")
+ artifact_ids: list[str] = Field(default_factory=list)
+ roles: list[AgentRole] = Field(default_factory=list)
+ detail: dict[str, object] = Field(default_factory=dict)
+
+
+def detect_conflicts(
+ *,
+ artifacts: Iterable[Artifact],
+ edges: Iterable[TraceEdge],
+ current_versions: Mapping[str, int],
+ runs: Iterable[AgentRun],
+ concerns_by_run: Mapping[str, list[str]] | None = None,
+) -> list[Conflict]:
+ """Return every inconsistency detectable from current project state.
+
+ Args:
+ artifacts: Every artifact in the project.
+ edges: Every traceability edge.
+ current_versions: Artifact ID to current version.
+ runs: Agent runs, used for confidence and attribution.
+ concerns_by_run: Concerns each run raised about upstream work.
+
+ Returns:
+ Conflicts, blocking ones first, then in detection order.
+ """
+ artifact_list = list(artifacts)
+ run_list = list(runs)
+ by_id = {artifact.id: artifact for artifact in artifact_list}
+
+ found: list[Conflict] = [
+ *_stale_derivations(edges, current_versions, by_id),
+ *_duplicate_authority(artifact_list),
+ *_unresolved_concerns(run_list, concerns_by_run or {}),
+ *_low_confidence(run_list),
+ ]
+
+ found.sort(key=lambda conflict: 0 if conflict.severity is ConflictSeverity.BLOCKING else 1)
+ return found
+
+
+def blocking(conflicts: Iterable[Conflict]) -> list[Conflict]:
+ """Filter to conflicts that must halt progress."""
+ return [conflict for conflict in conflicts if conflict.severity is ConflictSeverity.BLOCKING]
+
+
+def _stale_derivations(
+ edges: Iterable[TraceEdge],
+ current_versions: Mapping[str, int],
+ by_id: Mapping[str, Artifact],
+) -> list[Conflict]:
+ """Artifacts whose upstream has advanced past the version they consumed.
+
+ Blocking. This is the exact question `04_Existing_Solutions.md` says no tool
+ answers — continuing to build on a stale derivation is how an architecture
+ quietly stops matching its requirements.
+ """
+ conflicts: list[Conflict] = []
+
+ for stale in stale_edges(edges, current_versions):
+ downstream = by_id.get(stale.edge.downstream_artifact_id)
+ upstream = by_id.get(stale.edge.upstream_artifact_id)
+
+ downstream_title = downstream.title if downstream else stale.edge.downstream_artifact_id
+ upstream_title = upstream.title if upstream else stale.edge.upstream_artifact_id
+
+ conflicts.append(
+ Conflict(
+ kind=ConflictKind.STALE_DERIVATION,
+ severity=ConflictSeverity.BLOCKING,
+ summary=(
+ f"{downstream_title} was derived from {upstream_title} v"
+ f"{stale.edge.upstream_version}, which is now v"
+ f"{stale.current_upstream_version}"
+ ),
+ artifact_ids=[
+ stale.edge.downstream_artifact_id,
+ stale.edge.upstream_artifact_id,
+ ],
+ roles=[downstream.owner_role] if downstream else [],
+ detail={
+ "versions_behind": stale.versions_behind,
+ "trace_kind": stale.edge.kind.value,
+ },
+ )
+ )
+
+ return conflicts
+
+
+def _duplicate_authority(artifacts: Iterable[Artifact]) -> list[Conflict]:
+ """Two approved artifacts of the same type in one project.
+
+ Blocking. `15_Development_Guidelines.md` requires shared memory to be *the*
+ single source of truth; two approved system architectures means downstream
+ agents can read different answers to the same question.
+ """
+ approved: dict[tuple[str, str], list[Artifact]] = defaultdict(list)
+
+ for artifact in artifacts:
+ if artifact.status is ArtifactStatus.APPROVED and artifact.has_content:
+ approved[(artifact.type.value, artifact.stage.value)].append(artifact)
+
+ return [
+ Conflict(
+ kind=ConflictKind.DUPLICATE_AUTHORITY,
+ severity=ConflictSeverity.BLOCKING,
+ summary=(
+ f"{len(group)} approved '{artifact_type}' artifacts exist; "
+ "downstream agents would have competing sources of truth"
+ ),
+ artifact_ids=[artifact.id for artifact in group],
+ roles=sorted({artifact.owner_role for artifact in group}),
+ detail={"artifact_type": artifact_type, "stage": stage},
+ )
+ for (artifact_type, stage), group in approved.items()
+ if len(group) > 1
+ ]
+
+
+def _unresolved_concerns(
+ runs: Iterable[AgentRun], concerns_by_run: Mapping[str, list[str]]
+) -> list[Conflict]:
+ """Concerns an agent raised about upstream work.
+
+ Advisory rather than blocking: a concern is an agent's judgement, and a human
+ should weigh it. Escalating every one to blocking would make agents reluctant
+ to raise them, which is the opposite of what `02_Proposed_Solution.md` asks
+ for.
+ """
+ by_id = {run.id: run for run in runs}
+
+ return [
+ Conflict(
+ kind=ConflictKind.UNRESOLVED_CONCERN,
+ severity=ConflictSeverity.ADVISORY,
+ summary=f"{run.role.value.replace('_', ' ').title()}: {concern}",
+ artifact_ids=list(run.output_artifact_ids),
+ roles=[run.role],
+ detail={"run_id": run_id, "stage": run.stage.value},
+ )
+ for run_id, concerns in concerns_by_run.items()
+ if (run := by_id.get(run_id)) is not None
+ for concern in concerns
+ ]
+
+
+def _low_confidence(runs: Iterable[AgentRun]) -> list[Conflict]:
+ """Completed work the producing agent was not confident in.
+
+ Advisory: low confidence is an honest signal, and the correct response is
+ human review rather than halting. Treating it as blocking would pressure
+ agents toward inflated scores, defeating the safeguard.
+ """
+ return [
+ Conflict(
+ kind=ConflictKind.LOW_CONFIDENCE,
+ severity=ConflictSeverity.ADVISORY,
+ summary=(
+ f"{run.role.value.replace('_', ' ').title()} reported "
+ f"{run.confidence:.0%} confidence in {run.stage.value.replace('_', ' ')}"
+ ),
+ artifact_ids=list(run.output_artifact_ids),
+ roles=[run.role],
+ detail={"run_id": run.id, "confidence": run.confidence},
+ )
+ for run in runs
+ if run.confidence is not None and run.confidence < LOW_CONFIDENCE_THRESHOLD
+ ]
diff --git a/submissions/Victorious/apps/api/app/orchestration/dependencies.py b/submissions/Victorious/apps/api/app/orchestration/dependencies.py
new file mode 100644
index 00000000..72480cbf
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/dependencies.py
@@ -0,0 +1,231 @@
+"""Stage dependency and readiness rules.
+
+`05_AI_Agent_Architecture.md` lists "Track dependencies" among the Executive AI's
+responsibilities, and `12_Risk_Analysis.md` prescribes "dependency validation" as
+a mitigation for Agent Coordination Failure. These rules are that validation.
+
+Everything here is pure and deterministic. Whether a stage may run is a question
+about which artifacts exist and which approvals were granted — a computation, not
+a judgement. Asking a language model would introduce hallucination risk into the
+one place the platform must be exactly right.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from datetime import datetime
+from enum import StrEnum
+
+from app.domain.approvals import ApprovalKind, ApprovalRequest, ApprovalStatus
+from app.domain.artifacts import Artifact, ArtifactStatus, ArtifactType
+from app.domain.lifecycle import LifecycleStage
+
+#: Artifact types each stage requires from upstream before it may run.
+#:
+#: Derived from the per-agent outputs in `05_AI_Agent_Architecture.md` and the
+#: lifecycle in `09_MVP_Roadmap.md`. The first two stages have no requirements:
+#: `07_System_Architecture.md` mandates that a project begins from a name and a
+#: description alone.
+STAGE_INPUTS: dict[LifecycleStage, frozenset[ArtifactType]] = {
+ LifecycleStage.IDEA: frozenset(),
+ LifecycleStage.REQUIREMENT_DISCOVERY: frozenset(),
+ LifecycleStage.BUSINESS_VALIDATION: frozenset({ArtifactType.PRD}),
+ LifecycleStage.ARCHITECTURE: frozenset(
+ {ArtifactType.PRD, ArtifactType.BUSINESS_ANALYSIS}
+ ),
+ LifecycleStage.DEVELOPMENT_PLANNING: frozenset({ArtifactType.SYSTEM_ARCHITECTURE}),
+ LifecycleStage.IMPLEMENTATION: frozenset(
+ {ArtifactType.IMPLEMENTATION_PLAN, ArtifactType.SYSTEM_ARCHITECTURE}
+ ),
+ LifecycleStage.TESTING: frozenset(
+ {ArtifactType.ACCEPTANCE_CRITERIA, ArtifactType.REPOSITORY_STRUCTURE}
+ ),
+ LifecycleStage.DOCUMENTATION: frozenset(
+ {ArtifactType.SYSTEM_ARCHITECTURE, ArtifactType.REPOSITORY_STRUCTURE}
+ ),
+ LifecycleStage.DEPLOYMENT_PREPARATION: frozenset({ArtifactType.README}),
+}
+
+#: Approval that must be granted before a stage may execute.
+#:
+#: `09_MVP_Roadmap.md` requires human approval of Requirements, Architecture,
+#: Technology Stack, Major Engineering Decisions, and Final Code Generation.
+#: Three of those are structural properties of the lifecycle and are gated here:
+#:
+#: - requirements are signed off before anything is designed from them;
+#: - the architecture is signed off before work is planned against it;
+#: - an explicit go-ahead precedes code generation ("Final Code Generation").
+#:
+#: Technology Stack and Major Engineering Decisions are not stage-shaped — they
+#: arise from what an agent concludes — so agents raise them through
+#: `AgentOutput.requires_approval` instead.
+STAGE_GATES: dict[LifecycleStage, ApprovalKind] = {
+ LifecycleStage.ARCHITECTURE: ApprovalKind.REQUIREMENTS,
+ LifecycleStage.DEVELOPMENT_PLANNING: ApprovalKind.ARCHITECTURE,
+ LifecycleStage.IMPLEMENTATION: ApprovalKind.CODE_GENERATION,
+}
+
+
+class ReadinessStatus(StrEnum):
+ """Whether a stage may execute."""
+
+ READY = "ready"
+ MISSING_INPUTS = "missing_inputs"
+ AWAITING_APPROVAL = "awaiting_approval"
+ APPROVAL_REQUIRED = "approval_required"
+ """A gate applies and no request exists yet — one must be raised."""
+
+ BLOCKED_BY_REJECTION = "blocked_by_rejection"
+
+
+@dataclass(frozen=True)
+class Readiness:
+ """The outcome of evaluating a stage's preconditions."""
+
+ stage: LifecycleStage
+ status: ReadinessStatus
+ missing_inputs: frozenset[ArtifactType] = frozenset()
+ gate: ApprovalKind | None = None
+ approval_id: str | None = None
+ detail: str = ""
+
+ @property
+ def is_ready(self) -> bool:
+ return self.status is ReadinessStatus.READY
+
+
+@dataclass(frozen=True)
+class ProjectSnapshot:
+ """The facts readiness is evaluated against.
+
+ Passed explicitly rather than fetched inside, so the rules stay pure and each
+ orchestration pass reads shared memory exactly once.
+ """
+
+ artifacts: list[Artifact] = field(default_factory=list)
+ approvals: list[ApprovalRequest] = field(default_factory=list)
+
+ def approved_types(self) -> set[ArtifactType]:
+ """Artifact types present with content.
+
+ Presence, not approval, satisfies an *input* requirement: approval is
+ gated separately by ``STAGE_GATES``. Requiring both here would make the
+ gates unreachable, since a stage could never run to produce what its own
+ gate is meant to review.
+ """
+ return {artifact.type for artifact in self.artifacts if artifact.has_content}
+
+ def approval_for(self, kind: ApprovalKind) -> ApprovalRequest | None:
+ """Most recent approval request of a kind, if any."""
+ matching = [approval for approval in self.approvals if approval.kind is kind]
+ if not matching:
+ return None
+ return max(matching, key=lambda approval: approval.created_at)
+
+
+def evaluate_readiness(stage: LifecycleStage, snapshot: ProjectSnapshot) -> Readiness:
+ """Decide whether ``stage`` may execute now.
+
+ Inputs are checked before gates: an approval request to review requirements
+ that do not exist yet would be meaningless.
+ """
+ required = STAGE_INPUTS.get(stage, frozenset())
+ missing = required - snapshot.approved_types()
+
+ if missing:
+ return Readiness(
+ stage=stage,
+ status=ReadinessStatus.MISSING_INPUTS,
+ missing_inputs=frozenset(missing),
+ detail=(
+ f"{stage.value} requires "
+ + ", ".join(sorted(artifact.value for artifact in missing))
+ ),
+ )
+
+ gate = STAGE_GATES.get(stage)
+ if gate is None:
+ return Readiness(stage=stage, status=ReadinessStatus.READY)
+
+ approval = snapshot.approval_for(gate)
+
+ if approval is None:
+ return Readiness(
+ stage=stage,
+ status=ReadinessStatus.APPROVAL_REQUIRED,
+ gate=gate,
+ detail=f"{stage.value} requires human approval of {gate.value}",
+ )
+
+ if approval.status is ApprovalStatus.PENDING:
+ return Readiness(
+ stage=stage,
+ status=ReadinessStatus.AWAITING_APPROVAL,
+ gate=gate,
+ approval_id=approval.id,
+ detail=f"Waiting for a decision on {approval.title}",
+ )
+
+ if approval.status.unblocks_progress:
+ return Readiness(stage=stage, status=ReadinessStatus.READY, gate=gate)
+
+ # A rejection applies to the work as it stood when the reviewer saw it. Once
+ # the responsible agent has revised that work, the old decision is about a
+ # version that no longer exists, so a fresh gate is raised rather than the
+ # project being blocked forever by an answered objection.
+ if _revised_since(stage, snapshot, approval.decided_at):
+ return Readiness(
+ stage=stage,
+ status=ReadinessStatus.APPROVAL_REQUIRED,
+ gate=gate,
+ detail=f"{gate.value} was revised and needs a fresh decision",
+ )
+
+ return Readiness(
+ stage=stage,
+ status=ReadinessStatus.BLOCKED_BY_REJECTION,
+ gate=gate,
+ approval_id=approval.id,
+ detail=approval.feedback or f"{gate.value} was not approved",
+ )
+
+
+def _revised_since(
+ stage: LifecycleStage, snapshot: ProjectSnapshot, decided_at: datetime | None
+) -> bool:
+ """Whether the artifacts a stage's gate covers changed after a decision."""
+ if decided_at is None:
+ return False
+
+ required = STAGE_INPUTS.get(stage, frozenset())
+ return any(
+ artifact.updated_at > decided_at
+ for artifact in snapshot.artifacts
+ if artifact.type in required and artifact.has_content
+ )
+
+
+def gated_artifact_ids(
+ stage: LifecycleStage, snapshot: ProjectSnapshot
+) -> list[str]:
+ """Artifacts a stage's gate is asking the reviewer to sign off.
+
+ The artifacts the gate exists to protect are the ones the stage consumes, so
+ the Approval Center shows the reviewer exactly what the next stage will build
+ on rather than the whole project.
+ """
+ required = STAGE_INPUTS.get(stage, frozenset())
+ return [
+ artifact.id
+ for artifact in snapshot.artifacts
+ if artifact.type in required and artifact.has_content
+ ]
+
+
+def unapproved_artifact_ids(snapshot: ProjectSnapshot) -> list[str]:
+ """Artifacts with content that no human has signed off yet."""
+ return [
+ artifact.id
+ for artifact in snapshot.artifacts
+ if artifact.has_content and artifact.status is not ArtifactStatus.APPROVED
+ ]
diff --git a/submissions/Victorious/apps/api/app/orchestration/dispatcher.py b/submissions/Victorious/apps/api/app/orchestration/dispatcher.py
new file mode 100644
index 00000000..1a8a9c20
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/dispatcher.py
@@ -0,0 +1,141 @@
+"""Agent dispatch.
+
+The Executive AI routes work to specialists without knowing how any of them are
+built. `05_AI_Agent_Architecture.md` requires agents to be "independent, reusable
+module[s]" that do not modify each other's state; this protocol is the seam that
+keeps orchestration and implementation apart.
+
+It also means Milestone 3 is fully testable before Milestone 4 exists: the graph
+depends on this protocol, and tests supply stubs where the real organization will
+later be registered.
+"""
+
+from __future__ import annotations
+
+from typing import Protocol, runtime_checkable
+
+from app.agents.contracts import AgentResult
+from app.core.logging import get_logger
+from app.domain.errors import DependencyNotSatisfiedError
+from app.domain.lifecycle import STAGE_OWNERS, AgentRole, LifecycleStage
+
+logger = get_logger(__name__)
+
+
+@runtime_checkable
+class AgentDispatcher(Protocol):
+ """Routes a stage's work to the agent that owns it."""
+
+ def owns(self, stage: LifecycleStage) -> bool:
+ """Whether an agent is registered for this stage."""
+ ...
+
+ async def dispatch(
+ self, stage: LifecycleStage, project_id: str, *, feedback: str | None = None
+ ) -> AgentResult:
+ """Run the agent that owns ``stage``.
+
+ Args:
+ stage: Stage to execute.
+ project_id: Project being worked on.
+ feedback: Reviewer feedback from a rejected approval, passed through
+ so a rejection teaches rather than repeats.
+
+ Raises:
+ DependencyNotSatisfiedError: if no agent owns the stage.
+ """
+ ...
+
+
+class RegistryDispatcher:
+ """Dispatches to agents registered by the stage they perform.
+
+ Keyed by stage rather than role because a role can own more than one stage:
+ the Software Architect performs both architecture and development planning,
+ and the Documentation agent both documentation and deployment preparation.
+ Keying by role would dispatch development planning to the architecture agent,
+ which would then write its artifacts tagged with the wrong stage.
+
+ Agents are registered whole rather than under a supplied key, and the
+ registration is validated against :data:`app.domain.lifecycle.STAGE_OWNERS`.
+ That keeps one answer to "who owns this stage" — the domain's — and makes a
+ mis-registration impossible rather than merely unlikely.
+ """
+
+ def __init__(self) -> None:
+ self._agents: dict[LifecycleStage, _Runnable] = {}
+
+ def register(self, agent: _Runnable) -> None:
+ """Register an agent for the stage it declares.
+
+ Raises:
+ ValueError: if the stage is already filled, or if the agent's role
+ disagrees with the domain's owner for that stage.
+ """
+ stage = agent.stage
+ role = agent.role
+
+ if stage in self._agents:
+ raise ValueError(f"An agent is already registered for stage {stage.value}")
+
+ expected = STAGE_OWNERS.get(stage)
+ if expected is None:
+ raise ValueError(f"No engineering role owns stage {stage.value}")
+
+ if role is not expected:
+ raise ValueError(
+ f"{type(agent).__name__} declares role {role.value}, but "
+ f"{stage.value} is owned by {expected.value}"
+ )
+
+ self._agents[stage] = agent
+ logger.debug(
+ "Agent registered", extra={"stage": stage.value, "role": role.value}
+ )
+
+ def owns(self, stage: LifecycleStage) -> bool:
+ return stage in self._agents
+
+ async def dispatch(
+ self, stage: LifecycleStage, project_id: str, *, feedback: str | None = None
+ ) -> AgentResult:
+ agent = self._agents.get(stage)
+
+ if agent is None:
+ raise DependencyNotSatisfiedError(
+ "No agent is registered to perform this stage",
+ details={
+ "stage": stage.value,
+ "role": (owner.value if (owner := STAGE_OWNERS.get(stage)) else None),
+ },
+ )
+
+ return await agent.run(project_id, feedback=feedback)
+
+ @property
+ def registered_stages(self) -> list[LifecycleStage]:
+ """Stages currently covered, for diagnostics."""
+ return sorted(self._agents, key=lambda stage: stage.value)
+
+ @property
+ def registered_roles(self) -> list[AgentRole]:
+ """Distinct roles currently filled, for the Organization view."""
+ return sorted({agent.role for agent in self._agents.values()})
+
+
+@runtime_checkable
+class _Runnable(Protocol):
+ """What the dispatcher needs from an agent: its identity and one method.
+
+ Narrower than :class:`app.agents.base.BaseAgent` on purpose. The dispatcher
+ depends on the capability it uses, not on the base class, so a differently
+ implemented agent remains dispatchable.
+ """
+
+ @property
+ def role(self) -> AgentRole: ...
+
+ @property
+ def stage(self) -> LifecycleStage: ...
+
+ async def run(self, project_id: str, *, feedback: str | None = None) -> AgentResult: ...
diff --git a/submissions/Victorious/apps/api/app/orchestration/executive.py b/submissions/Victorious/apps/api/app/orchestration/executive.py
new file mode 100644
index 00000000..b6ca8b0f
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/executive.py
@@ -0,0 +1,902 @@
+"""The Executive AI (Engineering Director).
+
+`05_AI_Agent_Architecture.md` gives it nine responsibilities: receive user
+requests, maintain project state, route work between agents, track dependencies,
+resolve conflicts, maintain the project timeline, synchronize project context,
+handle approvals, and monitor the overall workflow. Its outputs are task
+assignments, updated workflow, and shared project state.
+
+`15_Development_Guidelines.md` is explicit about the boundary:
+
+ The Executive AI (Engineering Director) coordinates engineering activities
+ but does not directly perform engineering work.
+
+That rule is enforced structurally rather than by discipline. This class lives in
+``app.orchestration``, not ``app.agents``; it does not extend
+:class:`app.agents.base.BaseAgent`, and so it has no artifact-writing path at
+all. It cannot produce a PRD or an architecture even if a future change tried to
+make it — there is no code path from here to ``artifacts.create``.
+
+**Routing decisions are computed, not reasoned.** Whether a stage may run is a
+question about which artifacts exist and which approvals were granted, and
+:mod:`app.orchestration.dependencies` answers it deterministically.
+`12_Risk_Analysis.md` rates AI Hallucination a High risk; putting a language
+model in charge of dependency validation would place that risk in the mechanism
+whose job is preventing it.
+
+The Executive uses reasoning for exactly one thing: writing the prose a human
+reads at an approval gate. Even there, a provider failure falls back to
+deterministic text — `09_MVP_Roadmap.md` makes approval non-negotiable, so the
+gate cannot depend on a working model.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from datetime import UTC, datetime
+from enum import StrEnum
+
+from pydantic import BaseModel, Field
+
+from app.core.config import ReviewSettings
+from app.core.logging import get_correlation_id, get_logger
+from app.domain.agents import AgentMessage, AgentRun, AgentRunStatus
+from app.domain.approvals import ApprovalKind, ApprovalRequest, ApprovalStatus
+from app.domain.artifacts import ArtifactStatus, ArtifactType
+from app.domain.events import EventType, ProjectEvent
+from app.domain.lifecycle import (
+ ROLE_TITLES,
+ STAGE_OWNERS,
+ STAGE_SEQUENCE,
+ AgentRole,
+ LifecycleStage,
+ StageStatus,
+ stage_index,
+)
+from app.domain.projects import Project, StageState
+from app.events.bus import EventBus
+from app.llm.provider import CompletionRequest, LLMProvider, Message, Role
+from app.memory.repository import SharedMemory
+from app.orchestration.conflicts import Conflict, ConflictKind, blocking, detect_conflicts
+from app.orchestration.dependencies import (
+ STAGE_INPUTS,
+ ProjectSnapshot,
+ Readiness,
+ ReadinessStatus,
+ evaluate_readiness,
+ gated_artifact_ids,
+)
+
+logger = get_logger(__name__)
+
+#: Gates an agent raises about its own output, rather than gates the lifecycle
+#: imposes on a stage's inputs. The distinction decides which artifacts the
+#: reviewer is shown.
+_AGENT_RAISED_GATES = frozenset(
+ {ApprovalKind.TECHNOLOGY_SELECTION, ApprovalKind.ENGINEERING_DECISION}
+)
+
+
+class CoordinationAction(StrEnum):
+ """What the Executive AI has decided should happen next."""
+
+ EXECUTE_STAGE = "execute_stage"
+ REQUEST_APPROVAL = "request_approval"
+ AWAIT_APPROVAL = "await_approval"
+ HALT_BLOCKED = "halt_blocked"
+ COMPLETE = "complete"
+
+
+@dataclass(frozen=True)
+class CoordinationDecision:
+ """One coordination decision, with the reasoning that produced it.
+
+ Every field a user would need to understand *why* the workflow did what it
+ did. `12_Risk_Analysis.md` mitigates Loss of User Trust with "explainable
+ reasoning" and "transparent decision history"; an opaque router would fail
+ that regardless of how correct it was.
+ """
+
+ action: CoordinationAction
+ stage: LifecycleStage | None = None
+ role: AgentRole | None = None
+ readiness: Readiness | None = None
+ gate: ApprovalKind | None = None
+ approval_id: str | None = None
+ conflicts: list[Conflict] = field(default_factory=list)
+ rationale: str = ""
+
+ @property
+ def halts(self) -> bool:
+ return self.action in {
+ CoordinationAction.AWAIT_APPROVAL,
+ CoordinationAction.HALT_BLOCKED,
+ CoordinationAction.COMPLETE,
+ }
+
+
+class ApprovalNarration(BaseModel):
+ """Prose the Executive AI writes for an approval gate.
+
+ The three fields `10_UI_UX_Plan.md` requires a reviewer to see. The remaining
+ two — which agents were involved, and the downstream impact — are facts, not
+ prose, and are computed rather than written.
+ """
+
+ title: str = Field(max_length=200, description="What is being approved.")
+ what_changed: str = Field(description="Plain-language description of the work under review.")
+ why: str = Field(description="Why the organization produced it this way.")
+
+
+class ExecutiveAI:
+ """Coordinates the engineering organization. Performs no engineering work."""
+
+ role = AgentRole.EXECUTIVE
+
+ def __init__(
+ self,
+ memory: SharedMemory,
+ provider: LLMProvider,
+ events: EventBus,
+ review: ReviewSettings | None = None,
+ ) -> None:
+ self._memory = memory
+ self._provider = provider
+ self._events = events
+ # Defaulted so an Executive can be constructed without review settings and
+ # behaves exactly as it did before the review layer existed.
+ self._review = review or ReviewSettings()
+
+ @property
+ def title(self) -> str:
+ return ROLE_TITLES[AgentRole.EXECUTIVE]
+
+ # --- Assessment -----------------------------------------------------------
+
+ async def assess(self, project_id: str) -> CoordinationDecision:
+ """Decide what the organization should do next.
+
+ Reads shared memory once and evaluates deterministically. Blocking
+ conflicts take precedence over everything: proceeding while an
+ architecture is known to be derived from superseded requirements would
+ compound the inconsistency rather than surface it.
+ """
+ project = await self._memory.projects.get(project_id)
+ snapshot = await self._snapshot(project_id)
+ conflicts = await self._detect(project_id)
+
+ # An agent that judged its own output too consequential to proceed on
+ # blocks everything until a human answers. `09_MVP_Roadmap.md` requires
+ # approval of technology selection and major engineering decisions, and
+ # neither is stage-shaped — they arise from what an agent concludes.
+ if (pending := await self._agent_requested_gate(project_id, snapshot)) is not None:
+ run, kind = pending
+ return CoordinationDecision(
+ action=CoordinationAction.REQUEST_APPROVAL,
+ stage=run.stage,
+ role=run.role,
+ gate=kind,
+ conflicts=conflicts,
+ rationale=(
+ f"The {ROLE_TITLES[run.role]} asked for review before the "
+ f"organization proceeds: {run.approval_reason or 'no reason given'}"
+ ),
+ )
+
+ next_stage = self._next_incomplete_stage(project)
+
+ if next_stage is None:
+ # A finished project is not a frozen one. Changing a requirement
+ # after delivery is the case `04_Existing_Solutions.md` says nothing
+ # on the market handles, so the check for stale work has to happen
+ # here too — not only while stages remain to run.
+ if outstanding := self._outstanding(blocking(conflicts), project, snapshot):
+ return self._conflict_decision(
+ self._earliest_affected(outstanding, snapshot), outstanding, conflicts
+ )
+
+ return CoordinationDecision(
+ action=CoordinationAction.COMPLETE,
+ conflicts=conflicts,
+ rationale="Every lifecycle stage is complete.",
+ )
+
+ readiness = evaluate_readiness(next_stage, snapshot)
+ role = STAGE_OWNERS.get(next_stage)
+
+ match readiness.status:
+ case ReadinessStatus.READY:
+ # Conflicts block engineering work, not the act of asking a human.
+ # Checking them here rather than before the readiness evaluation
+ # is what lets a project recover from a rejection: revising an
+ # artifact necessarily makes its downstream stale, and if that
+ # halted everything the revised work could never be re-approved.
+ outstanding = self._outstanding(blocking(conflicts), project, snapshot)
+ if outstanding:
+ return self._conflict_decision(next_stage, outstanding, conflicts)
+
+ # The Executive consults the engineering review before committing
+ # a specialist to build on upstream work. Advisory by default:
+ # `13_Demo_and_Pitch.md` favours a demonstration that runs, and a
+ # quality score is a signal to weigh, not an authority to obey.
+ # `VICTORIOUS_REVIEW__BLOCKING=true` promotes it to a gate.
+ if failing := await self._failing_reviews(project_id, next_stage, snapshot):
+ if self._review.blocking:
+ return CoordinationDecision(
+ action=CoordinationAction.HALT_BLOCKED,
+ stage=next_stage,
+ role=role,
+ readiness=readiness,
+ conflicts=conflicts,
+ rationale=(
+ f"{len(failing)} upstream artifact(s) scored below "
+ f"{self._review.revision_threshold} in engineering "
+ f"review: {failing[0]}"
+ ),
+ )
+ logger.info(
+ "Advancing despite low review scores (advisory mode)",
+ extra={
+ "project_id": project_id,
+ "stage": next_stage.value,
+ "below_threshold": len(failing),
+ },
+ )
+
+ return CoordinationDecision(
+ action=CoordinationAction.EXECUTE_STAGE,
+ stage=next_stage,
+ role=role,
+ readiness=readiness,
+ conflicts=conflicts,
+ rationale=(
+ f"{next_stage.value.replace('_', ' ').title()} is ready; "
+ f"assigning to the {ROLE_TITLES[role] if role else 'organization'}."
+ ),
+ )
+
+ case ReadinessStatus.APPROVAL_REQUIRED:
+ return CoordinationDecision(
+ action=CoordinationAction.REQUEST_APPROVAL,
+ stage=next_stage,
+ role=role,
+ readiness=readiness,
+ gate=readiness.gate,
+ conflicts=conflicts,
+ rationale=readiness.detail,
+ )
+
+ case ReadinessStatus.AWAITING_APPROVAL:
+ return CoordinationDecision(
+ action=CoordinationAction.AWAIT_APPROVAL,
+ stage=next_stage,
+ readiness=readiness,
+ gate=readiness.gate,
+ approval_id=readiness.approval_id,
+ conflicts=conflicts,
+ rationale=readiness.detail,
+ )
+
+ case _:
+ return CoordinationDecision(
+ action=CoordinationAction.HALT_BLOCKED,
+ stage=next_stage,
+ readiness=readiness,
+ conflicts=conflicts,
+ rationale=readiness.detail,
+ )
+
+ async def _detect(self, project_id: str) -> list[Conflict]:
+ """Run every conflict detector against current project state."""
+ artifacts = await self._memory.artifacts.list_for_project(project_id)
+ edges = await self._memory.traces.list_for_project(project_id)
+ versions = await self._memory.artifacts.current_versions(project_id)
+ runs = await self._memory.runs.list_for_project(project_id)
+
+ return detect_conflicts(
+ artifacts=artifacts,
+ edges=edges,
+ current_versions=versions,
+ runs=runs,
+ )
+
+ async def _failing_reviews(
+ self, project_id: str, stage: LifecycleStage, snapshot: ProjectSnapshot
+ ) -> list[str]:
+ """Upstream artifacts this stage consumes that failed engineering review.
+
+ Scoped to the stage's *inputs* rather than the whole project: the question
+ is whether it is safe to build on what this stage is about to read, not
+ whether every artifact anywhere is sound. A weak deployment plan should
+ not block architecture.
+ """
+ required = STAGE_INPUTS.get(stage, frozenset())
+ if not required:
+ return []
+
+ failing: list[str] = []
+ for artifact in snapshot.artifacts:
+ if artifact.type not in required or not artifact.has_content:
+ continue
+
+ review = await self._memory.reviews.for_artifact(
+ artifact.id, artifact.current_version
+ )
+ if review is not None and review.quality_score < self._review.revision_threshold:
+ failing.append(f"{artifact.title} ({review.quality_score}/100)")
+
+ return failing
+
+ @staticmethod
+ def _outstanding(
+ fatal: list[Conflict], project: Project, snapshot: ProjectSnapshot
+ ) -> list[Conflict]:
+ """Drop blocking conflicts the workflow is already on its way to fixing.
+
+ A stale artifact whose stage is queued to run again is not something a
+ human needs to decide about — the specialist that owns it will rebuild it
+ against the current upstream on the next pass. Asking for approval to fix
+ something already scheduled to be fixed trains users to click through
+ gates, which is how a safeguard stops working.
+
+ Only stale derivations are filtered this way. Every other blocking
+ conflict describes a state no rerun resolves.
+ """
+ stage_by_artifact = {
+ artifact.id: artifact.stage for artifact in snapshot.artifacts
+ }
+ settled = {
+ state.stage for state in project.stages if state.status is StageStatus.COMPLETED
+ }
+
+ def already_scheduled(conflict: Conflict) -> bool:
+ if conflict.kind is not ConflictKind.STALE_DERIVATION:
+ return False
+ downstream = conflict.artifact_ids[0] if conflict.artifact_ids else None
+ stage = stage_by_artifact.get(downstream or "")
+ return stage is not None and stage not in settled
+
+ return [conflict for conflict in fatal if not already_scheduled(conflict)]
+
+ @staticmethod
+ def _earliest_affected(
+ conflicts: list[Conflict], snapshot: ProjectSnapshot
+ ) -> LifecycleStage:
+ """The earliest lifecycle stage a set of conflicts touches.
+
+ Rebuilding starts from the earliest affected stage, because anything
+ later derives from it and would otherwise be regenerated twice.
+ """
+ stage_by_artifact = {artifact.id: artifact.stage for artifact in snapshot.artifacts}
+ stages = [
+ stage
+ for conflict in conflicts
+ for artifact_id in conflict.artifact_ids
+ if (stage := stage_by_artifact.get(artifact_id)) is not None
+ ]
+ return min(stages, key=stage_index) if stages else LifecycleStage.IDEA
+
+ @staticmethod
+ def _conflict_decision(
+ stage: LifecycleStage, fatal: list[Conflict], conflicts: list[Conflict]
+ ) -> CoordinationDecision:
+ """Decide what a blocking conflict means for the workflow.
+
+ Stale derivations are recoverable: the upstream moved, and the agents
+ that built on it can rebuild against the current version. That is a
+ re-synchronisation, and `12_Risk_Analysis.md` puts changes of that
+ consequence behind a human — regenerating work the user has already
+ approved is not a decision the organization should make alone.
+
+ Anything else blocking — two competing approved artifacts, say — is a
+ state the organization cannot resolve by rerunning anyone, so it stops
+ and says so.
+ """
+ stale = [
+ conflict for conflict in fatal if conflict.kind is ConflictKind.STALE_DERIVATION
+ ]
+
+ if len(stale) == len(fatal):
+ return CoordinationDecision(
+ action=CoordinationAction.REQUEST_APPROVAL,
+ stage=stage,
+ gate=ApprovalKind.RESYNCHRONISATION,
+ conflicts=conflicts,
+ rationale=(
+ f"{len(stale)} artifact(s) were derived from work that has since "
+ "changed. Approving re-synchronisation reruns the affected "
+ "specialists against the current version."
+ ),
+ )
+
+ return CoordinationDecision(
+ action=CoordinationAction.HALT_BLOCKED,
+ stage=stage,
+ conflicts=conflicts,
+ rationale=(
+ f"{len(fatal)} blocking conflict(s) must be resolved before work "
+ f"continues: {fatal[0].summary}"
+ ),
+ )
+
+ async def _agent_requested_gate(
+ self, project_id: str, snapshot: ProjectSnapshot
+ ) -> tuple[AgentRun, ApprovalKind] | None:
+ """Find an agent's request for review that no human has answered.
+
+ The kind is inferred from what the run produced: a run that selected
+ technologies is a Technology Selection gate, anything else a Major
+ Engineering Decision. Both are named in `09_MVP_Roadmap.md`, and the
+ distinction is what the reviewer sees in the Approval Center.
+
+ Returns ``None`` once a request of that kind exists for the run's stage —
+ raised or already decided — so the gate is not raised twice.
+ """
+ runs = await self._memory.runs.list_for_project(project_id)
+
+ for run in sorted(runs, key=lambda item: item.started_at):
+ if not run.requires_approval or run.status is not AgentRunStatus.COMPLETED:
+ continue
+
+ produced = [
+ artifact
+ for artifact in snapshot.artifacts
+ if artifact.id in set(run.output_artifact_ids)
+ ]
+ kind = (
+ ApprovalKind.TECHNOLOGY_SELECTION
+ if any(a.type is ArtifactType.TECHNOLOGY_DECISION for a in produced)
+ else ApprovalKind.ENGINEERING_DECISION
+ )
+
+ already = [
+ approval
+ for approval in snapshot.approvals
+ if approval.kind is kind and approval.stage is run.stage
+ ]
+ if not already:
+ return run, kind
+
+ return None
+
+ async def _snapshot(self, project_id: str) -> ProjectSnapshot:
+ return ProjectSnapshot(
+ artifacts=await self._memory.artifacts.list_for_project(project_id),
+ approvals=await self._memory.approvals.list_for_project(project_id),
+ )
+
+ @staticmethod
+ def _next_incomplete_stage(project: Project) -> LifecycleStage | None:
+ """First stage in lifecycle order that has not completed.
+
+ ``IDEA`` is skipped: it is the state a project starts in, not work the
+ organization performs. `07_System_Architecture.md` has the Executive
+ begin requirement discovery as soon as a project exists.
+ """
+ completed = {state.stage for state in project.stages if state.is_complete}
+
+ return next(
+ (
+ stage
+ for stage in STAGE_SEQUENCE
+ if stage is not LifecycleStage.IDEA and stage not in completed
+ ),
+ None,
+ )
+
+ # --- Routing --------------------------------------------------------------
+
+ def assignment_for(
+ self, decision: CoordinationDecision, snapshot_artifact_ids: list[str]
+ ) -> AgentMessage:
+ """Build the structured assignment sent to a specialist.
+
+ `05_AI_Agent_Architecture.md` requires agents to communicate through
+ structured messages routed by the Executive AI rather than free-form
+ conversation, with every interaction carrying sender, receiver, task,
+ context, dependencies, decision, confidence, and required actions. This
+ is that message, and it is recorded on the routing event so the workspace
+ can show the actual assignment rather than a description of one.
+ """
+ if decision.stage is None or decision.role is None:
+ raise ValueError("Only an execute decision produces an assignment")
+
+ return AgentMessage(
+ sender=AgentRole.EXECUTIVE,
+ receiver=decision.role,
+ task=f"Perform {decision.stage.value.replace('_', ' ')} for this project.",
+ context_artifact_ids=snapshot_artifact_ids,
+ dependencies=sorted(
+ artifact.value
+ for artifact in (
+ decision.readiness.missing_inputs if decision.readiness else frozenset()
+ )
+ ),
+ decision=decision.rationale,
+ required_actions=[
+ "Produce the artifacts your role owns for this stage.",
+ "Declare the upstream artifacts each output was derived from.",
+ "Raise concerns about upstream work rather than working around them.",
+ ],
+ )
+
+ # --- Project state --------------------------------------------------------
+
+ async def project_state(self, project_id: str) -> Project:
+ """Current project state, for callers that need to check before acting."""
+ return await self._memory.projects.get(project_id)
+
+ async def mark_stage(
+ self, project_id: str, stage: LifecycleStage, status: StageStatus
+ ) -> Project:
+ """Record a stage transition on the project.
+
+ "Maintain project state" and "maintain project timeline" from
+ `05_AI_Agent_Architecture.md`. The Engineering Timeline reads this.
+ """
+ project = await self._memory.projects.get(project_id)
+ now = datetime.now(UTC)
+
+ existing = project.stage_state(stage)
+ if existing is None:
+ existing = StageState(stage=stage)
+ project.stages.append(existing)
+
+ existing.status = status
+ if status is StageStatus.IN_PROGRESS and existing.started_at is None:
+ existing.started_at = now
+ if status is StageStatus.COMPLETED:
+ existing.completed_at = now
+
+ project.current_stage = stage
+ return await self._memory.projects.update(project)
+
+ # --- Approvals ------------------------------------------------------------
+
+ async def record_decision(
+ self, approval_id: str, decision: ApprovalStatus, feedback: str | None
+ ) -> ApprovalRequest:
+ """Apply a human's decision and reopen work if they rejected it.
+
+ "Handle approvals" from `05_AI_Agent_Architecture.md`. Lives here rather
+ than in the API router because deciding a gate is a coordination act with
+ several consequences, and the router should not own any of them.
+
+ Approving marks the reviewed artifacts approved: the human signed off on
+ exactly those, and leaving them in draft would make the approval
+ invisible everywhere else in the workspace.
+
+ Rejecting reopens the stage that *produced* the artifacts, not the stage
+ the gate was blocking. The problem is with the work, so the specialist
+ that did it runs again — with the reviewer's feedback in its context, so
+ the rejection teaches rather than repeats.
+ """
+ request = await self._memory.approvals.get(approval_id)
+ request.status = decision
+ request.feedback = feedback
+ request.decided_at = datetime.now(UTC)
+ await self._memory.approvals.update(request)
+
+ if request.kind is ApprovalKind.RESYNCHRONISATION:
+ # Re-synchronisation is not a sign-off on content; it is permission to
+ # rebuild. Approving reruns the affected specialists against the
+ # current upstream, and declining leaves the stale work in place with
+ # the staleness still visible in the workspace.
+ if decision.unblocks_progress:
+ await self._resynchronise(request)
+ elif decision.unblocks_progress:
+ await self._approve_artifacts(request)
+ else:
+ await self._reopen_producing_stages(request)
+
+ await self.publish(
+ request.project_id,
+ (
+ EventType.APPROVAL_GRANTED
+ if decision.unblocks_progress
+ else EventType.APPROVAL_REJECTED
+ ),
+ f"{decision.value.replace('_', ' ').title()}: {request.title}",
+ {
+ "approval_id": request.id,
+ "kind": request.kind.value,
+ "artifact_ids": request.artifact_ids,
+ },
+ stage=request.stage,
+ )
+
+ logger.info(
+ "Approval decided",
+ extra={
+ "project_id": request.project_id,
+ "approval_id": approval_id,
+ "decision": decision.value,
+ },
+ )
+ return request
+
+ async def _approve_artifacts(self, request: ApprovalRequest) -> None:
+ """Mark everything the gate covered as approved."""
+ for artifact_id in request.artifact_ids:
+ artifact = await self._memory.artifacts.get(artifact_id)
+ artifact.status = ArtifactStatus.APPROVED
+ await self._memory.artifacts.update(artifact)
+
+ async def _resynchronise(self, request: ApprovalRequest) -> None:
+ """Reopen the stages whose work has fallen out of date.
+
+ Only stages that actually produced a stale artifact are reopened —
+ selective regeneration, not rebuilding the project. An artifact whose
+ upstream never moved is still valid and is left alone, which is the
+ difference between propagating a change and starting over.
+ """
+ stale = await self._memory.traces.stale_edges(request.project_id)
+ if not stale:
+ return
+
+ affected: set[LifecycleStage] = set()
+ for entry in stale:
+ artifact = await self._memory.artifacts.get(entry.edge.downstream_artifact_id)
+ affected.add(artifact.stage)
+
+ project = await self._memory.projects.get(request.project_id)
+ project.stages = [
+ state.model_copy(update={"status": StageStatus.PENDING, "completed_at": None})
+ if state.stage in affected
+ else state
+ for state in project.stages
+ ]
+ await self._memory.projects.update(project)
+
+ await self.publish(
+ request.project_id,
+ EventType.ARTIFACT_MARKED_STALE,
+ f"Re-synchronising {len(affected)} stage(s) against the revised upstream",
+ {
+ "stages": sorted(stage.value for stage in affected),
+ "stale_edges": len(stale),
+ },
+ )
+
+ logger.info(
+ "Re-synchronisation approved",
+ extra={
+ "project_id": request.project_id,
+ "stages": sorted(stage.value for stage in affected),
+ },
+ )
+
+ async def _reopen_producing_stages(self, request: ApprovalRequest) -> None:
+ """Send the rejected work back to whoever produced it.
+
+ Derived from the artifacts under review rather than declared on the
+ request, so a gate covering several stages reopens all of them and
+ neither the gate nor the reviewer has to know which.
+ """
+ project = await self._memory.projects.get(request.project_id)
+ reopened: set[LifecycleStage] = set()
+
+ for artifact_id in request.artifact_ids:
+ artifact = await self._memory.artifacts.get(artifact_id)
+ reopened.add(artifact.stage)
+
+ if not reopened:
+ return
+
+ project.stages = [
+ state.model_copy(update={"status": StageStatus.PENDING, "completed_at": None})
+ if state.stage in reopened
+ else state
+ for state in project.stages
+ ]
+ await self._memory.projects.update(project)
+
+ logger.info(
+ "Stages reopened after rejection",
+ extra={
+ "project_id": request.project_id,
+ "stages": sorted(stage.value for stage in reopened),
+ },
+ )
+
+ async def rejection_feedback_for(
+ self, project_id: str, stage: LifecycleStage
+ ) -> str | None:
+ """Return reviewer feedback from the most recent rejection of a stage.
+
+ "Handle approvals" from `05_AI_Agent_Architecture.md`. Passed into the
+ agent on re-run so a rejection teaches rather than repeats.
+ """
+ approvals = await self._memory.approvals.list_for_project(project_id)
+
+ for approval in sorted(approvals, key=lambda a: a.created_at, reverse=True):
+ if approval.stage is stage and approval.feedback:
+ return approval.feedback
+ return None
+
+ async def raise_gate(
+ self, project_id: str, stage: LifecycleStage, gate: ApprovalKind
+ ) -> ApprovalRequest:
+ """Create the approval request blocking a stage.
+
+ Computes the downstream impact before the reviewer decides, which is what
+ `10_UI_UX_Plan.md` requires the Approval Center to show — the
+ consequences of approving, seen in advance rather than discovered after.
+ """
+ snapshot = await self._snapshot(project_id)
+
+ # A stage gate protects what the *next* stage will consume; an
+ # agent-requested gate reviews what that agent just *produced*. Showing
+ # the reviewer the wrong set would make the decision meaningless.
+ artifact_ids = (
+ [
+ artifact.id
+ for artifact in snapshot.artifacts
+ if artifact.stage is stage and artifact.has_content
+ ]
+ if gate in _AGENT_RAISED_GATES
+ else gated_artifact_ids(stage, snapshot)
+ )
+
+ narration = await self._narrate(project_id, stage, gate, artifact_ids)
+
+ impact = None
+ if artifact_ids:
+ impact = await self._memory.traces.analyse_impact(project_id, artifact_ids[0])
+
+ involved = sorted(
+ {
+ artifact.owner_role
+ for artifact in snapshot.artifacts
+ if artifact.id in set(artifact_ids)
+ }
+ )
+
+ request = await self._memory.approvals.create(
+ ApprovalRequest(
+ project_id=project_id,
+ kind=gate,
+ stage=stage,
+ title=narration.title,
+ what_changed=narration.what_changed,
+ why=narration.why,
+ requested_by=AgentRole.EXECUTIVE,
+ agents_involved=involved,
+ artifact_ids=artifact_ids,
+ impact=impact,
+ )
+ )
+
+ await self.publish(
+ project_id,
+ EventType.APPROVAL_REQUESTED,
+ f"{self.title} requested approval: {narration.title}",
+ {
+ "approval_id": request.id,
+ "kind": gate.value,
+ "stage": stage.value,
+ "artifact_ids": artifact_ids,
+ "impacted_count": len(impact.impacted) if impact else 0,
+ },
+ stage=stage,
+ )
+
+ logger.info(
+ "Approval gate raised",
+ extra={
+ "project_id": project_id,
+ "approval_id": request.id,
+ "kind": gate.value,
+ "stage": stage.value,
+ },
+ )
+ return request
+
+ async def _narrate(
+ self,
+ project_id: str,
+ stage: LifecycleStage,
+ gate: ApprovalKind,
+ artifact_ids: list[str],
+ ) -> ApprovalNarration:
+ """Write the reviewer-facing prose for a gate.
+
+ Falls back to deterministic text on any provider failure. A human
+ approval gate that cannot be raised because a language model is
+ unavailable would defeat the safeguard entirely.
+ """
+ fallback = self._fallback_narration(stage, gate, artifact_ids)
+
+ try:
+ project = await self._memory.projects.get(project_id)
+ summaries: list[str] = []
+
+ for artifact_id in artifact_ids[:6]:
+ resolved = await self._memory.artifacts.get_version(artifact_id)
+ summaries.append(
+ f"- **{resolved.artifact.title}** "
+ f"({resolved.artifact.type.value}, v{resolved.version.version}): "
+ f"{resolved.version.summary or 'no summary'}"
+ )
+
+ response = await self._provider.complete_structured(
+ CompletionRequest(
+ system=(
+ "You are the Engineering Director of an AI software engineering "
+ "organization. Write the summary a human reviewer reads before "
+ "approving a stage transition. Be specific and factual about what "
+ "the organization produced and why. Do not invent detail that is "
+ "not present. Do not recommend approval or rejection — the human "
+ "decides."
+ ),
+ messages=[
+ Message(
+ role=Role.USER,
+ content=(
+ f"Project: {project.name}\n"
+ f"Description: {project.description}\n\n"
+ f"Approval required: {gate.value}\n"
+ f"Blocking stage: {stage.value}\n\n"
+ "Artifacts under review:\n" + ("\n".join(summaries) or "- none")
+ ),
+ )
+ ],
+ fixture_key=f"executive.gate.{gate.value}",
+ metadata={"role": "executive", "gate": gate.value},
+ ),
+ ApprovalNarration,
+ )
+ return response.value
+
+ except Exception:
+ logger.warning(
+ "Approval narration unavailable; using deterministic text",
+ extra={"project_id": project_id, "gate": gate.value},
+ exc_info=True,
+ )
+ return fallback
+
+ @staticmethod
+ def _fallback_narration(
+ stage: LifecycleStage, gate: ApprovalKind, artifact_ids: list[str]
+ ) -> ApprovalNarration:
+ """Deterministic gate prose, used when reasoning is unavailable."""
+ gate_label = gate.value.replace("_", " ")
+ stage_label = stage.value.replace("_", " ")
+
+ return ApprovalNarration(
+ title=f"Approve {gate_label} before {stage_label}",
+ what_changed=(
+ f"The organization produced {len(artifact_ids)} artifact(s) that "
+ f"{stage_label} will build on."
+ ),
+ why=(
+ f"{stage_label.title()} consumes this work directly. Approving it "
+ "here prevents downstream engineering from being derived from "
+ "output you have not reviewed."
+ ),
+ )
+
+ # --- Observability --------------------------------------------------------
+
+ async def publish(
+ self,
+ project_id: str,
+ event_type: EventType,
+ summary: str,
+ payload: dict[str, object],
+ *,
+ stage: LifecycleStage | None = None,
+ ) -> None:
+ """Record a coordination event."""
+ await self._events.publish(
+ ProjectEvent(
+ project_id=project_id,
+ type=event_type,
+ stage=stage,
+ role=AgentRole.EXECUTIVE,
+ summary=summary,
+ payload=payload,
+ correlation_id=get_correlation_id(),
+ )
+ )
diff --git a/submissions/Victorious/apps/api/app/orchestration/graph.py b/submissions/Victorious/apps/api/app/orchestration/graph.py
new file mode 100644
index 00000000..f30b5aad
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/graph.py
@@ -0,0 +1,268 @@
+"""The engineering workflow graph.
+
+`08_Technology_Stack.md` specifies LangGraph as the agent workflow engine. It is
+used here for what it is good at — declarative nodes, conditional routing, and a
+compiled executable graph — and deliberately *not* for checkpointing. Persisting
+workflow state in a checkpointer would create a second source of truth about
+where a project stands, contradicting `15_Development_Guidelines.md` and creating
+exactly the Context Drift risk `12_Risk_Analysis.md` warns about. See ADR-0009.
+
+The graph is a loop:
+
+ START ──► coordinate ──► execute ──┐
+ │ │
+ ├──► gate ──► END │
+ │ │
+ └──► END │
+ ▲ │
+ └────────────────────┘
+
+Only ``coordinate`` decides anything — it is the Executive AI. ``execute`` and
+``gate`` each carry out exactly one instruction and route nowhere of their own
+accord. That asymmetry is the structural form of
+`15_Development_Guidelines.md`'s rule that the Executive coordinates while
+specialists perform.
+"""
+
+from __future__ import annotations
+
+from typing import Final, Literal
+
+from langgraph.graph import END, START, StateGraph
+
+from app.core.logging import get_logger
+from app.domain.approvals import ApprovalKind
+from app.domain.events import EventType
+from app.domain.lifecycle import LifecycleStage, StageStatus
+from app.orchestration.dispatcher import AgentDispatcher
+from app.orchestration.executive import CoordinationAction, ExecutiveAI
+from app.orchestration.state import OrchestrationState
+
+logger = get_logger(__name__)
+
+COORDINATE: Final = "coordinate"
+EXECUTE: Final = "execute"
+GATE: Final = "gate"
+
+#: LangGraph's terminal node name. Bound to a Final so the routing functions can
+#: declare precise Literal return types.
+TERMINAL: Final = "__end__"
+
+
+def build_workflow(executive: ExecutiveAI, dispatcher: AgentDispatcher): # type: ignore[no-untyped-def]
+ """Compile the engineering workflow.
+
+ Args:
+ executive: Coordinates the organization.
+ dispatcher: Routes stage work to the agent that owns it.
+
+ Returns:
+ A compiled LangGraph application, invoked via ``ainvoke``.
+ """
+
+ async def coordinate(state: OrchestrationState) -> OrchestrationState:
+ """Ask the Executive AI what happens next, and record the decision.
+
+ The only node that decides. It also performs the Executive's own routing
+ responsibility — publishing the structured assignment that
+ `05_AI_Agent_Architecture.md` requires — so the specialist nodes receive
+ an instruction rather than deriving one.
+ """
+ project_id = state["project_id"]
+ decision = await executive.assess(project_id)
+
+ conflicts = [conflict.model_dump(mode="json") for conflict in decision.conflicts]
+ stage_value = decision.stage.value if decision.stage else None
+
+ await executive.publish(
+ project_id,
+ EventType.STAGE_BLOCKED if decision.halts else EventType.STAGE_STARTED,
+ f"{executive.title}: {decision.rationale}",
+ {
+ "action": decision.action.value,
+ "stage": stage_value,
+ "role": decision.role.value if decision.role else None,
+ "conflicts": len(conflicts),
+ },
+ stage=decision.stage,
+ )
+
+ base: OrchestrationState = {
+ **state,
+ "next_action": decision.action.value,
+ "stage": stage_value,
+ "conflicts": conflicts,
+ }
+
+ if decision.action is CoordinationAction.EXECUTE_STAGE:
+ assignment = executive.assignment_for(decision, [])
+ await executive.publish(
+ project_id,
+ EventType.AGENT_PROGRESS,
+ (
+ f"{executive.title} assigned "
+ f"{assignment.task.rstrip('.')} to "
+ f"{assignment.receiver.value.replace('_', ' ')}"
+ ),
+ {"assignment": assignment.model_dump(mode="json")},
+ stage=decision.stage,
+ )
+ return base
+
+ if decision.action is CoordinationAction.REQUEST_APPROVAL:
+ return {**base, "gate": decision.gate.value if decision.gate else None}
+
+ return {
+ **base,
+ "halted": True,
+ "halt_action": decision.action.value,
+ "halt_reason": decision.rationale,
+ "pending_approval_id": decision.approval_id,
+ }
+
+ async def execute(state: OrchestrationState) -> OrchestrationState:
+ """Dispatch the assigned stage to the agent that owns it."""
+ project_id = state["project_id"]
+ raw_stage = state.get("stage")
+
+ if raw_stage is None:
+ return {
+ **state,
+ "halted": True,
+ "halt_action": CoordinationAction.HALT_BLOCKED.value,
+ "halt_reason": "No stage was assigned",
+ }
+
+ stage = LifecycleStage(raw_stage)
+
+ if not dispatcher.owns(stage):
+ await executive.mark_stage(project_id, stage, StageStatus.BLOCKED)
+ return {
+ **state,
+ "halted": True,
+ "halt_action": CoordinationAction.HALT_BLOCKED.value,
+ "halt_reason": f"No agent is registered to perform {stage.value}",
+ }
+
+ await executive.mark_stage(project_id, stage, StageStatus.IN_PROGRESS)
+ feedback = await executive.rejection_feedback_for(project_id, stage)
+
+ try:
+ await dispatcher.dispatch(stage, project_id, feedback=feedback)
+ # Broad by design: any agent failure must halt this traversal gracefully
+ # rather than propagate. `12_Risk_Analysis.md` requires the system to fail
+ # gracefully and recover predictably, and the next `advance` call retries.
+ except Exception as exc: # noqa: BLE001
+ # The agent has already recorded its own failure and published an
+ # event. The graph's job here is to stop, not to re-report.
+ await executive.mark_stage(project_id, stage, StageStatus.BLOCKED)
+ logger.warning(
+ "Stage execution failed",
+ extra={"project_id": project_id, "stage": stage.value},
+ )
+ return {
+ **state,
+ "halted": True,
+ "halt_action": CoordinationAction.HALT_BLOCKED.value,
+ "halt_reason": f"{stage.value} failed: {type(exc).__name__}",
+ "error": f"{type(exc).__name__}: {exc}",
+ }
+
+ await executive.mark_stage(project_id, stage, StageStatus.COMPLETED)
+ await executive.publish(
+ project_id,
+ EventType.STAGE_COMPLETED,
+ f"{stage.value.replace('_', ' ').title()} completed",
+ {"stage": stage.value},
+ stage=stage,
+ )
+
+ return {
+ **state,
+ "executed_stages": [*state.get("executed_stages", []), stage.value],
+ }
+
+ async def gate(state: OrchestrationState) -> OrchestrationState:
+ """Raise the approval request blocking this stage, then stop.
+
+ The traversal genuinely ends here. `12_Risk_Analysis.md` mitigates
+ Excessive Automation with "human approval checkpoints"; a gate that
+ notified the user while work continued would not be one.
+ """
+ project_id = state["project_id"]
+ raw_stage = state.get("stage")
+ raw_gate = state.get("gate")
+
+ if raw_stage is None or raw_gate is None:
+ return {
+ **state,
+ "halted": True,
+ "halt_action": CoordinationAction.HALT_BLOCKED.value,
+ "halt_reason": "Approval was required but no gate was identified",
+ }
+
+ stage = LifecycleStage(raw_stage)
+ request = await executive.raise_gate(project_id, stage, ApprovalKind(raw_gate))
+
+ # Only a stage that has not run yet is "awaiting approval". A gate an
+ # agent raised about work it just finished must not un-complete that
+ # stage, or the specialist would be dispatched again once the gate
+ # cleared — doing the same work twice.
+ project = await executive.project_state(project_id)
+ stage_state = project.stage_state(stage)
+ if stage_state is None or stage_state.status is not StageStatus.COMPLETED:
+ await executive.mark_stage(project_id, stage, StageStatus.AWAITING_APPROVAL)
+
+ return {
+ **state,
+ "halted": True,
+ "halt_action": CoordinationAction.AWAIT_APPROVAL.value,
+ "halt_reason": f"Awaiting human approval: {request.title}",
+ "pending_approval_id": request.id,
+ }
+
+ def route_after_coordinate(
+ state: OrchestrationState,
+ ) -> Literal["execute", "gate", "__end__"]:
+ """Route on the Executive's recorded decision.
+
+ Reads one field rather than inferring intent from several, so a new
+ coordination action cannot accidentally fall through to execution.
+ """
+ match state.get("next_action"):
+ case CoordinationAction.EXECUTE_STAGE.value:
+ return EXECUTE
+ case CoordinationAction.REQUEST_APPROVAL.value:
+ return GATE
+ case _:
+ return TERMINAL
+
+ def route_after_execute(state: OrchestrationState) -> Literal["coordinate", "__end__"]:
+ """Return to coordination unless execution halted the traversal.
+
+ Without this check the edge back to ``coordinate`` is unconditional, and a
+ stage that fails — or that no agent owns — is re-dispatched forever: the
+ Executive would correctly re-assess it as ready, dispatch it, watch it
+ fail, and loop until the recursion limit. Execution must be able to stop
+ the traversal, and this is the only place it can.
+ """
+ return TERMINAL if state.get("halted") else COORDINATE
+
+ graph = StateGraph(OrchestrationState)
+ graph.add_node(COORDINATE, coordinate)
+ graph.add_node(EXECUTE, execute)
+ graph.add_node(GATE, gate)
+
+ graph.add_edge(START, COORDINATE)
+ graph.add_conditional_edges(
+ COORDINATE, route_after_coordinate, {EXECUTE: EXECUTE, GATE: GATE, END: END}
+ )
+ # A successful stage returns to coordination. That loop is what makes this a
+ # workflow rather than a fixed pipeline: the Executive re-assesses after every
+ # unit of work, so a change made mid-run is seen on the next pass.
+ graph.add_conditional_edges(
+ EXECUTE, route_after_execute, {COORDINATE: COORDINATE, END: END}
+ )
+ graph.add_edge(GATE, END)
+
+ return graph.compile()
diff --git a/submissions/Victorious/apps/api/app/orchestration/runner.py b/submissions/Victorious/apps/api/app/orchestration/runner.py
new file mode 100644
index 00000000..37354e73
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/runner.py
@@ -0,0 +1,140 @@
+"""Public entry point for advancing a project's engineering workflow.
+
+One traversal runs until the organization can make no further progress without a
+human: an approval gate, a blocking conflict, a stage nobody owns, or completion.
+
+Resumption is simply calling :meth:`OrchestrationRunner.advance` again. Because
+every node reads current facts from shared memory rather than from accumulated
+graph state, a traversal that begins after an approval is granted sees the new
+decision immediately — and works across process restarts, which an in-memory
+checkpointer would not. See ADR-0009.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from app.core.config import ReviewSettings
+from app.core.logging import get_logger
+from app.domain.errors import NotFoundError
+from app.domain.lifecycle import LifecycleStage
+from app.events.bus import EventBus
+from app.llm.provider import LLMProvider
+from app.memory.repository import SharedMemory
+from app.orchestration.dispatcher import AgentDispatcher
+from app.orchestration.executive import CoordinationAction, ExecutiveAI
+from app.orchestration.graph import build_workflow
+from app.orchestration.state import RECURSION_LIMIT, OrchestrationState
+
+logger = get_logger(__name__)
+
+
+@dataclass(frozen=True)
+class OrchestrationOutcome:
+ """The result of one traversal."""
+
+ project_id: str
+ executed_stages: list[LifecycleStage] = field(default_factory=list)
+ halt_action: CoordinationAction | None = None
+ halt_reason: str = ""
+ pending_approval_id: str | None = None
+ conflicts: list[dict[str, object]] = field(default_factory=list)
+ error: str | None = None
+
+ @property
+ def awaiting_approval(self) -> bool:
+ return self.halt_action is CoordinationAction.AWAIT_APPROVAL
+
+ @property
+ def is_complete(self) -> bool:
+ return self.halt_action is CoordinationAction.COMPLETE
+
+ @property
+ def is_blocked(self) -> bool:
+ return self.halt_action is CoordinationAction.HALT_BLOCKED
+
+ @property
+ def made_progress(self) -> bool:
+ return bool(self.executed_stages)
+
+
+class OrchestrationRunner:
+ """Drives the engineering workflow for a project."""
+
+ def __init__(
+ self,
+ memory: SharedMemory,
+ provider: LLMProvider,
+ events: EventBus,
+ dispatcher: AgentDispatcher,
+ review: ReviewSettings | None = None,
+ ) -> None:
+ self._memory = memory
+ self._executive = ExecutiveAI(memory, provider, events, review)
+ self._dispatcher = dispatcher
+ # Compiled once: the graph's shape is fixed, and rebuilding it per
+ # request would pay compilation cost on every advance.
+ self._workflow = build_workflow(self._executive, dispatcher)
+
+ @property
+ def executive(self) -> ExecutiveAI:
+ """The Executive AI, exposed for direct assessment without a traversal."""
+ return self._executive
+
+ async def advance(self, project_id: str) -> OrchestrationOutcome:
+ """Advance the project as far as it can go without a human.
+
+ Args:
+ project_id: Project to advance.
+
+ Returns:
+ What was executed and why it stopped.
+
+ Raises:
+ NotFoundError: if the project does not exist.
+ """
+ if not await self._memory.projects.exists(project_id):
+ raise NotFoundError("Project not found", details={"project_id": project_id})
+
+ initial: OrchestrationState = {
+ "project_id": project_id,
+ "next_action": None,
+ "stage": None,
+ "gate": None,
+ "executed_stages": [],
+ "halted": False,
+ "halt_action": None,
+ "halt_reason": None,
+ "pending_approval_id": None,
+ "conflicts": [],
+ "error": None,
+ }
+
+ final: OrchestrationState = await self._workflow.ainvoke(
+ initial, config={"recursion_limit": RECURSION_LIMIT}
+ )
+
+ halt_action = final.get("halt_action")
+
+ outcome = OrchestrationOutcome(
+ project_id=project_id,
+ executed_stages=[
+ LifecycleStage(stage) for stage in final.get("executed_stages", [])
+ ],
+ halt_action=CoordinationAction(halt_action) if halt_action else None,
+ halt_reason=final.get("halt_reason") or "",
+ pending_approval_id=final.get("pending_approval_id"),
+ conflicts=final.get("conflicts", []),
+ error=final.get("error"),
+ )
+
+ logger.info(
+ "Orchestration traversal finished",
+ extra={
+ "project_id": project_id,
+ "executed": [stage.value for stage in outcome.executed_stages],
+ "halt_action": outcome.halt_action.value if outcome.halt_action else None,
+ "conflicts": len(outcome.conflicts),
+ },
+ )
+ return outcome
diff --git a/submissions/Victorious/apps/api/app/orchestration/state.py b/submissions/Victorious/apps/api/app/orchestration/state.py
new file mode 100644
index 00000000..e646e34f
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/orchestration/state.py
@@ -0,0 +1,54 @@
+"""Orchestration graph state.
+
+Deliberately thin. `15_Development_Guidelines.md` makes shared memory the single
+source of truth, so this envelope carries only what one traversal needs to route
+itself — never project knowledge. Every node reads current facts from shared
+memory rather than from accumulated state.
+
+That choice is what makes the workflow resumable across process restarts:
+resuming is re-entering the graph, which re-reads memory. See ADR-0009.
+"""
+
+from __future__ import annotations
+
+from typing import TypedDict
+
+#: Nodes traversed per stage (coordinate + execute), times nine stages, plus
+#: gates and a margin. LangGraph's default of 25 would halt a healthy run
+#: partway through, which reads as a mysterious stall rather than a limit.
+RECURSION_LIMIT = 120
+
+
+class OrchestrationState(TypedDict, total=False):
+ """State carried through one traversal of the workflow graph."""
+
+ project_id: str
+
+ next_action: str | None
+ """The :class:`~app.orchestration.executive.CoordinationAction` the Executive
+ decided on. The single field routing reads, so a new action cannot
+ accidentally fall through to execution."""
+
+ stage: str | None
+ """Stage the current decision concerns. ``None`` before the first assessment."""
+
+ gate: str | None
+ """The :class:`~app.domain.approvals.ApprovalKind` blocking the stage, when
+ the decision was to request approval."""
+
+ executed_stages: list[str]
+ """Stages executed during this traversal, for the returned outcome."""
+
+ halted: bool
+ halt_action: str | None
+ """The :class:`~app.orchestration.executive.CoordinationAction` that stopped it."""
+
+ halt_reason: str | None
+ pending_approval_id: str | None
+
+ conflicts: list[dict[str, object]]
+ """Conflicts observed at the halt, serialised for the API response."""
+
+ error: str | None
+ """Set when a dispatched agent failed. The traversal stops; the run record
+ and the failure event are written by the agent itself."""
diff --git a/submissions/Victorious/apps/api/app/review/__init__.py b/submissions/Victorious/apps/api/app/review/__init__.py
new file mode 100644
index 00000000..3a23fe4a
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/review/__init__.py
@@ -0,0 +1,23 @@
+"""Engineering review layer.
+
+Reviews every artifact the organization produces: a deterministic structural
+score, optionally sharpened by a bounded model judgement.
+
+Sits beside ``app.agents`` and below ``app.orchestration``. It imports memory,
+llm, domain, and core — and nothing from Mutagent. Helix specifies, evaluates,
+and optimizes this reviewer at development time; `07_System_Architecture.md`
+keeps Mutagent out of the runtime execution path, and
+``tests/test_architecture.py`` enforces that.
+"""
+
+from app.review.checks import CheckResult, is_first_stage, run_checks
+from app.review.reviewer import MAX_ADJUSTMENT, EngineeringReviewer, ReviewJudgement
+
+__all__ = [
+ "MAX_ADJUSTMENT",
+ "CheckResult",
+ "EngineeringReviewer",
+ "ReviewJudgement",
+ "is_first_stage",
+ "run_checks",
+]
diff --git a/submissions/Victorious/apps/api/app/review/checks.py b/submissions/Victorious/apps/api/app/review/checks.py
new file mode 100644
index 00000000..7b47f232
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/review/checks.py
@@ -0,0 +1,276 @@
+"""Deterministic structural checks over a produced artifact.
+
+These carry the weight of the review score, and reasoning only adjusts it within
+a bounded range. Three reasons:
+
+- `12_Risk_Analysis.md` rates AI Hallucination a High risk. A reviewer that is
+ purely a language model can invent a weakness, or miss a real one, and a score
+ built entirely on that is not evidence of anything.
+- The demo runs on recorded fixtures. If the score came from replayed prose,
+ every artifact would score identically and the number would be theatre.
+ Structural checks read the *actual* artifact, so scores genuinely differ.
+- A structural finding is a fact a user can verify — "declares no upstream" is
+ checkable. An opinion is not.
+
+Each check returns points and, when it deducts, a finding explaining why. The
+findings are the reviewer's evidence, and they survive into the stored review so
+the workspace can show what was measured rather than only what was concluded.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from app.domain.artifacts import Artifact, ArtifactType, ArtifactVersion
+from app.domain.lifecycle import LifecycleStage
+from app.domain.reviews import ReviewFinding
+
+#: Weights, summing to 100. Traceability and substance dominate because they are
+#: the properties the whole platform depends on: an artifact nobody can trace is
+#: invisible to impact analysis, and an empty one is not work.
+TRACEABILITY_POINTS = 25
+CONTENT_POINTS = 25
+SUBSTANCE_POINTS = 20
+CONFIDENCE_POINTS = 15
+COMPLETENESS_POINTS = 15
+
+#: A body shorter than this is a heading with nothing under it.
+_THIN_BODY_CHARS = 400
+_RICH_BODY_CHARS = 1200
+
+#: Structured fields each artifact type is expected to carry. Absence is a real
+#: defect: downstream agents read these fields, not the prose.
+_EXPECTED_CONTENT: dict[ArtifactType, tuple[str, ...]] = {
+ ArtifactType.PRD: ("functional_requirements", "objective"),
+ ArtifactType.USER_STORIES: ("user_stories",),
+ ArtifactType.ACCEPTANCE_CRITERIA: ("criteria",),
+ ArtifactType.BUSINESS_ANALYSIS: ("feasibility",),
+ ArtifactType.GAP_ANALYSIS: ("gaps",),
+ ArtifactType.RISK_REGISTER: ("risks",),
+ ArtifactType.SYSTEM_ARCHITECTURE: ("components", "style"),
+ ArtifactType.TECHNOLOGY_DECISION: ("choices",),
+ ArtifactType.API_CONTRACT: ("endpoints",),
+ ArtifactType.DATABASE_SCHEMA: ("entities",),
+ ArtifactType.IMPLEMENTATION_PLAN: ("tasks",),
+ ArtifactType.REPOSITORY_STRUCTURE: ("tree",),
+ ArtifactType.SOURCE_FILE: ("content", "path"),
+ ArtifactType.TEST_PLAN: ("strategy",),
+ ArtifactType.TEST_CASES: ("test_cases",),
+ ArtifactType.COVERAGE_REPORT: ("entries",),
+ ArtifactType.DEPLOYMENT_PLAN: ("checklist",),
+}
+
+
+@dataclass
+class CheckResult:
+ """The outcome of running every structural check."""
+
+ score: int
+ strengths: list[ReviewFinding] = field(default_factory=list)
+ weaknesses: list[ReviewFinding] = field(default_factory=list)
+ suggestions: list[ReviewFinding] = field(default_factory=list)
+
+ def as_evidence(self) -> str:
+ """Render the findings for a reviewing model's context.
+
+ The model is shown what was measured so its judgement builds on the
+ evidence rather than re-deriving it — and so it cannot contradict a fact.
+ """
+ lines = [f"Structural score: {self.score}/100"]
+ for label, findings in (
+ ("Verified strengths", self.strengths),
+ ("Detected weaknesses", self.weaknesses),
+ ):
+ if findings:
+ lines.append(f"\n{label}:")
+ lines.extend(f"- {finding.text}" for finding in findings)
+ return "\n".join(lines)
+
+
+def run_checks(
+ artifact: Artifact,
+ version: ArtifactVersion,
+ *,
+ upstream_count: int,
+ is_first_stage: bool,
+) -> CheckResult:
+ """Score an artifact on properties that can be measured rather than judged."""
+ result = CheckResult(score=0)
+
+ result.score += _traceability(artifact, upstream_count, is_first_stage, result)
+ result.score += _structured_content(artifact, version, result)
+ result.score += _substance(version, result)
+ result.score += _confidence(version, result)
+ result.score += _type_completeness(artifact, version, result)
+
+ return result
+
+
+def _traceability(
+ artifact: Artifact, upstream_count: int, is_first_stage: bool, result: CheckResult
+) -> int:
+ """Does the artifact declare what it was derived from?
+
+ The first stage legitimately has no upstream, so it is credited in full
+ rather than penalised for a property it cannot have.
+ """
+ if is_first_stage:
+ result.strengths.append(
+ ReviewFinding(text="Originates the project; no upstream expected.")
+ )
+ return TRACEABILITY_POINTS
+
+ if upstream_count == 0:
+ result.weaknesses.append(
+ ReviewFinding(
+ text=(
+ "Declares no upstream artifacts, so a change to its inputs "
+ "could not flag it as out of date."
+ )
+ )
+ )
+ return 0
+
+ result.strengths.append(
+ ReviewFinding(text=f"Traced to {upstream_count} upstream artifact(s).")
+ )
+ return TRACEABILITY_POINTS
+
+
+def _structured_content(
+ artifact: Artifact, version: ArtifactVersion, result: CheckResult
+) -> int:
+ """Is there structured content for downstream agents to read?
+
+ Downstream specialists read fields, not prose. An artifact whose content is
+ empty forces the next agent to parse a document, which is exactly the
+ coupling the platform's structured contracts exist to avoid.
+ """
+ if not version.content:
+ result.weaknesses.append(
+ ReviewFinding(
+ text=(
+ "Carries no structured content; downstream agents would have "
+ "to parse the prose."
+ )
+ )
+ )
+ return 0
+
+ populated = [
+ key
+ for key, value in version.content.items()
+ if value not in (None, "", [], {})
+ ]
+
+ if not populated:
+ result.weaknesses.append(
+ ReviewFinding(text="Structured content is present but every field is empty.")
+ )
+ return 0
+
+ result.strengths.append(
+ ReviewFinding(text=f"Structured content populated across {len(populated)} field(s).")
+ )
+ return CONTENT_POINTS
+
+
+def _substance(version: ArtifactVersion, result: CheckResult) -> int:
+ """Is the rendered document substantive, or a heading with nothing under it?"""
+ length = len(version.body_markdown)
+
+ if length < _THIN_BODY_CHARS:
+ result.weaknesses.append(
+ ReviewFinding(
+ text=f"Document is thin ({length} characters); likely under-specified."
+ )
+ )
+ result.suggestions.append(
+ ReviewFinding(text="Expand with the detail a downstream engineer would need.")
+ )
+ return SUBSTANCE_POINTS // 4
+
+ if length < _RICH_BODY_CHARS:
+ result.strengths.append(ReviewFinding(text="Document has adequate detail."))
+ return SUBSTANCE_POINTS * 3 // 4
+
+ result.strengths.append(
+ ReviewFinding(text=f"Document is detailed ({length} characters).")
+ )
+ return SUBSTANCE_POINTS
+
+
+def _confidence(version: ArtifactVersion, result: CheckResult) -> int:
+ """How confident was the specialist that produced it?
+
+ `12_Risk_Analysis.md` names confidence scoring as a hallucination mitigation.
+ The mitigation only bites if something acts on a low score — this does.
+ """
+ confidence = version.confidence
+
+ if confidence is None:
+ result.weaknesses.append(
+ ReviewFinding(text="Producing agent reported no confidence.")
+ )
+ return 0
+
+ if confidence < 0.5:
+ result.weaknesses.append(
+ ReviewFinding(
+ text=f"Producing agent reported low confidence ({confidence:.0%})."
+ )
+ )
+ result.suggestions.append(
+ ReviewFinding(text="Route to a human before downstream work builds on it.")
+ )
+ return 0
+
+ if confidence < 0.75:
+ return CONFIDENCE_POINTS // 2
+
+ result.strengths.append(
+ ReviewFinding(text=f"Produced with {confidence:.0%} confidence.")
+ )
+ return CONFIDENCE_POINTS
+
+
+def _type_completeness(
+ artifact: Artifact, version: ArtifactVersion, result: CheckResult
+) -> int:
+ """Does the artifact carry the fields its type is supposed to carry?"""
+ expected = _EXPECTED_CONTENT.get(artifact.type)
+
+ if not expected:
+ # Documentation artifacts are prose by design; substance already covers
+ # them, and inventing a field requirement would penalise correct output.
+ return COMPLETENESS_POINTS
+
+ missing = [
+ key
+ for key in expected
+ if not version.content.get(key)
+ ]
+
+ if not missing:
+ result.strengths.append(
+ ReviewFinding(text=f"Carries every field expected of a {artifact.type.value}.")
+ )
+ return COMPLETENESS_POINTS
+
+ result.weaknesses.append(
+ ReviewFinding(
+ text=(
+ f"Missing expected field(s) for a {artifact.type.value}: "
+ + ", ".join(missing)
+ )
+ )
+ )
+ result.suggestions.append(
+ ReviewFinding(text=f"Populate {', '.join(missing)} so downstream agents can read it.")
+ )
+ return round(COMPLETENESS_POINTS * (1 - len(missing) / len(expected)))
+
+
+def is_first_stage(stage: LifecycleStage) -> bool:
+ """Whether a stage legitimately has no upstream to declare."""
+ return stage is LifecycleStage.REQUIREMENT_DISCOVERY
diff --git a/submissions/Victorious/apps/api/app/review/prompts/engineering_review.md b/submissions/Victorious/apps/api/app/review/prompts/engineering_review.md
new file mode 100644
index 00000000..fd34c282
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/review/prompts/engineering_review.md
@@ -0,0 +1,64 @@
+You review engineering artifacts produced by an AI Software Engineering
+Organization. A specialist has just produced the artifact below, and downstream
+specialists will build on it.
+
+Your judgement decides whether that is safe.
+
+## What you are adding
+
+A structural analysis has already run and is shown to you. It measured things
+that can be measured: whether the artifact declares its upstream, whether it
+carries structured content downstream agents can read, whether it is substantive,
+how confident its author was, and whether it has the fields its type requires.
+
+**Do not repeat those findings.** They are established. Your job is the judgement
+a structural check cannot make:
+
+- Is this **specific**, or does it describe a category of system rather than this
+ one? "The system should be fast" and "supports many users" are unfalsifiable.
+- Is it **internally consistent**? Do two statements contradict each other?
+- Would a competent engineer **implement this the way it was intended**, or are
+ there two defensible readings?
+- Does it record **reasoning**, or only conclusions? A decision without its
+ rationale cannot be reviewed later.
+- Is anything **obviously missing** that this artifact's type demands — an error
+ path, an authorisation rule, a boundary condition?
+
+## Scoring
+
+You do not set the score. You adjust it, by at most ±12.
+
+Adjust **upward** only for quality the structural checks genuinely cannot see:
+unusually clear reasoning, a well-argued trade-off, a gap the author caught
+themselves.
+
+Adjust **downward** for a real defect the checks missed: a contradiction, an
+unfalsifiable requirement, a decision with no rationale, a dangerous omission.
+
+Use `0` when the structural score already reflects the artifact. That is the
+common case and it is the right answer more often than not. Inflating every
+review teaches the reader to ignore the number.
+
+You cannot rescue a structurally broken artifact and you cannot condemn a sound
+one. That bound is deliberate.
+
+## Findings
+
+Every strength, weakness, and suggestion must point at **something in this
+artifact**. Quote or name it.
+
+"Requirements are well written" is not a finding. "FR-02 specifies the conflict
+behaviour for double bookings, which is the case most likely to be implemented
+wrongly" is.
+
+A suggestion must be actionable by the specialist that produced this — not a
+restatement of the weakness. If you cannot say what to do about a weakness, leave
+the suggestion out rather than padding it.
+
+Return no findings at all rather than manufacturing them. An empty list is a
+legitimate answer for good work, and it is more useful than invented criticism.
+
+## Tone
+
+Write for the engineer who produced this and the human who will approve it. Be
+direct, specific, and brief. No preamble, no praise sandwiches, no hedging.
diff --git a/submissions/Victorious/apps/api/app/review/reviewer.py b/submissions/Victorious/apps/api/app/review/reviewer.py
new file mode 100644
index 00000000..60dcbb00
--- /dev/null
+++ b/submissions/Victorious/apps/api/app/review/reviewer.py
@@ -0,0 +1,238 @@
+"""The engineering reviewer.
+
+Scores every artifact the organization produces. Composed of two layers:
+
+1. **Deterministic checks** (:mod:`app.review.checks`) measure structural
+ properties and set the score. They always run.
+2. **Reasoning**, when a provider is available, reads the artifact *and the
+ evidence from layer 1* and contributes prose plus a bounded score adjustment.
+
+Reasoning can move the score by at most :data:`MAX_ADJUSTMENT`. That cap is the
+whole design: a model can sharpen a judgement, but it cannot overturn a measured
+fact, and it cannot manufacture a high score for an artifact that declares no
+upstream and carries no structured content.
+
+**Fail-open, always.** A review is a quality signal, not a gate on production. If
+the provider errors, times out, or returns something unusable, the structural
+review stands and the agent run completes normally. An organization that stops
+working because its reviewer is unavailable would be worse than one that does not
+review at all.
+
+Helix (Mutagent's ADL conductor) specifies, evaluates, and optimizes *this
+reviewer* at development time. It is not invoked here: `07_System_Architecture.md`
+places Mutagent outside the runtime execution path, and nothing in this module
+imports it.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from pydantic import BaseModel, Field
+
+from app.core.config import ReviewSettings
+from app.core.logging import get_logger
+from app.domain.artifacts import Artifact, ArtifactVersion
+from app.domain.reviews import ArtifactReview, ReviewFinding, ReviewVerdict
+from app.llm.provider import CompletionRequest, LLMProvider, Message, Role
+from app.review.checks import CheckResult, is_first_stage, run_checks
+
+logger = get_logger(__name__)
+
+PROMPT_PATH = Path(__file__).parent / "prompts" / "engineering_review.md"
+
+#: The most a model may move the structural score, in either direction. Wide
+#: enough for a real judgement to matter, narrow enough that a hallucinated
+#: verdict cannot rescue a structurally broken artifact.
+MAX_ADJUSTMENT = 12
+
+#: How much of the artifact the reviewer reads. Enough to judge, bounded so a
+#: large generated file cannot blow the context budget.
+_BODY_EXCERPT_CHARS = 6000
+
+
+class ReviewJudgement(BaseModel):
+ """What the reviewing model returns."""
+
+ summary: str = Field(
+ max_length=300, description="One line a user reads in a list."
+ )
+ score_adjustment: int = Field(
+ ge=-MAX_ADJUSTMENT,
+ le=MAX_ADJUSTMENT,
+ description=(
+ "Adjustment to the structural score. Positive only for quality the "
+ "checks cannot see; negative for a defect they missed."
+ ),
+ )
+ strengths: list[str] = Field(default_factory=list, max_length=6)
+ weaknesses: list[str] = Field(default_factory=list, max_length=6)
+ suggestions: list[str] = Field(default_factory=list, max_length=6)
+
+
+class EngineeringReviewer:
+ """Reviews an artifact and returns a scored, evidenced verdict."""
+
+ def __init__(
+ self,
+ provider: LLMProvider | None,
+ settings: ReviewSettings,
+ ) -> None:
+ self._provider = provider
+ self._settings = settings
+
+ async def review(
+ self,
+ artifact: Artifact,
+ version: ArtifactVersion,
+ *,
+ upstream_count: int,
+ ) -> ArtifactReview:
+ """Score one version of one artifact.
+
+ Args:
+ artifact: The artifact under review.
+ version: The version whose content is being judged.
+ upstream_count: How many upstream artifacts it declared.
+
+ Returns:
+ A review. Never raises — a failure downgrades to the structural
+ review rather than propagating.
+ """
+ checks = run_checks(
+ artifact,
+ version,
+ upstream_count=upstream_count,
+ is_first_stage=is_first_stage(artifact.stage),
+ )
+
+ judgement = await self._judge(artifact, version, checks)
+
+ score = checks.score
+ strengths = list(checks.strengths)
+ weaknesses = list(checks.weaknesses)
+ suggestions = list(checks.suggestions)
+ summary = _structural_summary(checks)
+
+ # Only credit the provider when it actually contributed.
+ provider = self._provider if judgement is not None else None
+
+ if judgement is not None:
+ score = max(0, min(100, checks.score + judgement.score_adjustment))
+ summary = judgement.summary or summary
+ strengths += [
+ ReviewFinding(text=item, source="reasoning") for item in judgement.strengths
+ ]
+ weaknesses += [
+ ReviewFinding(text=item, source="reasoning") for item in judgement.weaknesses
+ ]
+ suggestions += [
+ ReviewFinding(text=item, source="reasoning")
+ for item in judgement.suggestions
+ ]
+
+ return ArtifactReview(
+ project_id=artifact.project_id,
+ artifact_id=artifact.id,
+ artifact_version=version.version,
+ stage=artifact.stage,
+ role=artifact.owner_role,
+ produced_by_run_id=version.produced_by_run_id,
+ quality_score=score,
+ verdict=self._verdict(score),
+ summary=summary,
+ strengths=strengths,
+ weaknesses=weaknesses,
+ suggestions=suggestions,
+ deterministic_score=checks.score,
+ reasoning_applied=judgement is not None,
+ reviewer_provider=provider.name if provider is not None else None,
+ reviewer_model=provider.model if provider is not None else None,
+ )
+
+ def _verdict(self, score: int) -> ReviewVerdict:
+ """Map a score onto a verdict using the configured thresholds."""
+ if score < self._settings.revision_threshold:
+ return ReviewVerdict.NEEDS_REVISION
+ if score < self._settings.strong_threshold:
+ return ReviewVerdict.APPROVED_WITH_SUGGESTIONS
+ return ReviewVerdict.APPROVED
+
+ async def _judge(
+ self, artifact: Artifact, version: ArtifactVersion, checks: CheckResult
+ ) -> ReviewJudgement | None:
+ """Ask a model for a judgement, or return ``None`` if unavailable.
+
+ Every failure path returns ``None`` deliberately. The caller then keeps
+ the structural review, and `reasoning_applied` records that no model
+ contributed — so the workspace shows an honest score rather than a
+ confident-looking one produced by nothing.
+ """
+ if self._provider is None or not self._settings.use_reasoning:
+ return None
+
+ try:
+ response = await self._provider.complete_structured(
+ CompletionRequest(
+ system=PROMPT_PATH.read_text(encoding="utf-8"),
+ messages=[
+ Message(
+ role=Role.USER,
+ content=_render_request(artifact, version, checks),
+ )
+ ],
+ # Keyed by artifact type so a recorded corpus stays readable
+ # and one recording covers every project of that shape.
+ fixture_key=f"review.{artifact.type.value}",
+ metadata={"stage": artifact.stage.value, "type": artifact.type.value},
+ max_tokens=1500,
+ ),
+ ReviewJudgement,
+ )
+ return response.value
+
+ # Broad by design: reviewing is fail-open, so ANY provider fault must
+ # degrade to the structural review rather than surface to the agent.
+ except Exception: # noqa: BLE001
+ logger.warning(
+ "Review reasoning unavailable; keeping the structural review",
+ extra={"artifact_id": artifact.id, "type": artifact.type.value},
+ )
+ return None
+
+
+def _render_request(
+ artifact: Artifact, version: ArtifactVersion, checks: CheckResult
+) -> str:
+ """Compose what the reviewing model sees."""
+ body = version.body_markdown[:_BODY_EXCERPT_CHARS]
+ truncated = len(version.body_markdown) > _BODY_EXCERPT_CHARS
+
+ return "\n".join(
+ [
+ f"# Artifact under review: {artifact.title}",
+ "",
+ f"- Type: {artifact.type.value}",
+ f"- Produced during: {artifact.stage.value}",
+ f"- By: {artifact.owner_role.value}",
+ f"- Version: {version.version}",
+ "",
+ "## Structural analysis already performed",
+ "",
+ checks.as_evidence(),
+ "",
+ "## Content",
+ "",
+ body + ("\n\n_(truncated)_" if truncated else ""),
+ ]
+ )
+
+
+def _structural_summary(checks: CheckResult) -> str:
+ """A summary for when no model contributed."""
+ if not checks.weaknesses:
+ return f"Structural review passed with {checks.score}/100; no defects detected."
+ return (
+ f"Structural review scored {checks.score}/100 with "
+ f"{len(checks.weaknesses)} issue(s) detected."
+ )
diff --git a/submissions/Victorious/apps/api/fixtures/README.md b/submissions/Victorious/apps/api/fixtures/README.md
new file mode 100644
index 00000000..2553c1d9
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/README.md
@@ -0,0 +1,10 @@
+# Recorded provider responses.
+
+Written by RecordingProvider, replayed by FixtureProvider. See
+docs/adr/0008-fixture-provider-and-fallback.md.
+
+Record them with:
+
+ VICTORIOUS_LLM__RECORD_FIXTURES=true
+
+against a live provider, then switch VICTORIOUS_LLM__PROVIDER back to `fixture`.
diff --git a/submissions/Victorious/apps/api/fixtures/business_analyst.business_validation.json b/submissions/Victorious/apps/api/fixtures/business_analyst.business_validation.json
new file mode 100644
index 00000000..cf1a4323
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/business_analyst.business_validation.json
@@ -0,0 +1,46 @@
+{
+ "value": {
+ "feasibility": "viable_with_changes",
+ "assessment": "The core workflows are sound; access control is underspecified.",
+ "validated_requirement_ids": [
+ "FR-01",
+ "FR-02"
+ ],
+ "questioned_requirement_ids": [
+ "NFR-01"
+ ],
+ "gaps": [
+ {
+ "area": "Access control",
+ "description": "NFR-01 names no roles or permission model.",
+ "severity": "high",
+ "recommendation": "Define roles before the architecture is designed.",
+ "requirement_ids": [
+ "NFR-01"
+ ]
+ }
+ ],
+ "risks": [
+ {
+ "description": "Clinical data handling may require regional certification.",
+ "impact": "high",
+ "likelihood": "possible",
+ "mitigation": "Confirm the applicable regime before go-live."
+ }
+ ],
+ "opportunities": [
+ "Appointment reminders would reduce no-shows."
+ ],
+ "reasoning": "FR-01 and FR-02 are specific and testable. NFR-01 names no roles and no permission model, so two engineers would implement it differently \u2014 I questioned it rather than letting the architect guess. The regulatory exposure follows from the domain and belongs on the risk register.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [],
+ "requires_approval": false,
+ "approval_reason": ""
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/documentation.deployment_preparation.json b/submissions/Victorious/apps/api/fixtures/documentation.deployment_preparation.json
new file mode 100644
index 00000000..abe0362e
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/documentation.deployment_preparation.json
@@ -0,0 +1,30 @@
+{
+ "value": {
+ "overview": "Containerised deployment behind a managed PostgreSQL instance.",
+ "checklist": [
+ "Run migrations and confirm the schema version."
+ ],
+ "environment_variables": [
+ "DATABASE_URL \u2014 PostgreSQL connection string"
+ ],
+ "containerisation": "FROM python:3.12-slim",
+ "rollback": [
+ "Redeploy the previous image; column drops are not reversible."
+ ],
+ "outstanding": [
+ "Authentication is not implemented.",
+ "FR-01 has no endpoint."
+ ],
+ "reasoning": "The plan follows the approved PostgreSQL and container decisions and introduces no infrastructure the organization did not choose. Environment variables are listed by name and purpose only. Authentication being absent and FR-01 having no endpoint genuinely block a production release.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [],
+ "requires_approval": false,
+ "approval_reason": ""
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/documentation.documentation.json b/submissions/Victorious/apps/api/fixtures/documentation.documentation.json
new file mode 100644
index 00000000..c26aa0ab
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/documentation.documentation.json
@@ -0,0 +1,20 @@
+{
+ "value": {
+ "readme": "# Hospital Management System\n\nScaffold only; not a running system.",
+ "api_documentation": "## POST /api/v1/appointments\n\nBooks an appointment.",
+ "architecture_document": "A modular monolith was chosen because one team owns it.",
+ "developer_guide": "Run migrations before starting the API.",
+ "changelog": "## 0.1.0\n\nInitial scaffold generated.",
+ "reasoning": "I documented what the organization actually decided: the modular monolith and why, the PostgreSQL trade-off, and the endpoints in the approved contract. The README states that this is a scaffold, because the QA coverage report and the engineer's own notes both say so.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [],
+ "requires_approval": false,
+ "approval_reason": ""
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/executive.gate.architecture.json b/submissions/Victorious/apps/api/fixtures/executive.gate.architecture.json
new file mode 100644
index 00000000..851efb5a
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/executive.gate.architecture.json
@@ -0,0 +1,11 @@
+{
+ "value": {
+ "title": "Approve the architecture before work is planned against it",
+ "what_changed": "The Software Architect proposed a modular monolith with patients and scheduling components, selected PostgreSQL over MongoDB, and defined the API contract and data model.",
+ "why": "The implementation plan and the generated scaffold both derive from this design. The technology selection in particular is expensive to reverse once code exists."
+ },
+ "usage": {
+ "input_tokens": 900,
+ "output_tokens": 220
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/executive.gate.code_generation.json b/submissions/Victorious/apps/api/fixtures/executive.gate.code_generation.json
new file mode 100644
index 00000000..9bb69630
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/executive.gate.code_generation.json
@@ -0,0 +1,11 @@
+{
+ "value": {
+ "title": "Authorise code generation",
+ "what_changed": "The implementation plan sequences the approved architecture into dependency-ordered tasks, starting with the data model and the booking conflict logic.",
+ "why": "This is the last gate before the organization writes the repository scaffold. Everything generated will trace back to this plan."
+ },
+ "usage": {
+ "input_tokens": 900,
+ "output_tokens": 220
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/executive.gate.requirements.json b/submissions/Victorious/apps/api/fixtures/executive.gate.requirements.json
new file mode 100644
index 00000000..fdd6e497
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/executive.gate.requirements.json
@@ -0,0 +1,11 @@
+{
+ "value": {
+ "title": "Approve the requirements before the architecture is designed",
+ "what_changed": "The Product Manager defined the functional and non-functional requirements, user stories, and acceptance criteria. The Business Analyst validated them and flagged access control as underspecified.",
+ "why": "Everything the architect designs derives from these requirements. Approving them here prevents a design being built on scope you have not reviewed."
+ },
+ "usage": {
+ "input_tokens": 900,
+ "output_tokens": 220
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/executive.gate.resynchronisation.json b/submissions/Victorious/apps/api/fixtures/executive.gate.resynchronisation.json
new file mode 100644
index 00000000..a55a87f2
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/executive.gate.resynchronisation.json
@@ -0,0 +1,11 @@
+{
+ "value": {
+ "title": "Approve re-synchronisation of work that is now out of date",
+ "what_changed": "Upstream work was revised after the artifacts below were derived from it, so those artifacts no longer reflect the current version.",
+ "why": "Approving reruns the affected specialists against the revised upstream. Declining leaves the work in place with its staleness still visible, so nothing is silently rebuilt."
+ },
+ "usage": {
+ "input_tokens": 900,
+ "output_tokens": 220
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/executive.gate.technology_selection.json b/submissions/Victorious/apps/api/fixtures/executive.gate.technology_selection.json
new file mode 100644
index 00000000..104417c5
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/executive.gate.technology_selection.json
@@ -0,0 +1,11 @@
+{
+ "value": {
+ "title": "Approve the technology selection",
+ "what_changed": "The Software Architect selected PostgreSQL over MongoDB, recording the alternative considered and the trade-off accepted.",
+ "why": "The architect judged this expensive to reverse once code exists and raised it for review itself. The schema, the API contract, and the generated scaffold all assume it."
+ },
+ "usage": {
+ "input_tokens": 900,
+ "output_tokens": 220
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/full_stack_engineer.implementation.json b/submissions/Victorious/apps/api/fixtures/full_stack_engineer.implementation.json
new file mode 100644
index 00000000..8e7d3950
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/full_stack_engineer.implementation.json
@@ -0,0 +1,33 @@
+{
+ "value": {
+ "repository_tree": [
+ "app/",
+ "app/patients/models.py",
+ "app/scheduling/api.py"
+ ],
+ "stack_summary": "FastAPI over PostgreSQL, matching the approved decisions.",
+ "files": [
+ {
+ "path": "app/patients/models.py",
+ "language": "python",
+ "purpose": "Patient record model, realising FR-01.",
+ "content": "class Patient:\n id: UUID\n name: str\n"
+ }
+ ],
+ "not_implemented": [
+ "Authentication flows",
+ "Database migrations"
+ ],
+ "reasoning": "I wrote the patient model because it is where the approved data design becomes concrete, and left package manifests and lint configuration to the tree. Authentication and migrations are genuinely absent and recorded as such \u2014 this is a scaffold, not a running system.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [],
+ "requires_approval": false,
+ "approval_reason": ""
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/product_manager.requirement_discovery.json b/submissions/Victorious/apps/api/fixtures/product_manager.requirement_discovery.json
new file mode 100644
index 00000000..7d073c86
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/product_manager.requirement_discovery.json
@@ -0,0 +1,68 @@
+{
+ "value": {
+ "objective": "Coordinate patient care, scheduling, and billing in one system.",
+ "target_users": [
+ "Reception staff",
+ "Doctors",
+ "Billing administrators"
+ ],
+ "functional_requirements": [
+ {
+ "id": "FR-01",
+ "title": "Register a patient",
+ "description": "Staff can create a patient record with demographics.",
+ "priority": "must",
+ "rationale": "Nothing else in the system works without a patient record."
+ },
+ {
+ "id": "FR-02",
+ "title": "Book an appointment",
+ "description": "Staff book a patient with a doctor at a time slot.",
+ "priority": "must",
+ "rationale": "Scheduling is the primary daily workflow."
+ }
+ ],
+ "non_functional_requirements": [
+ {
+ "id": "NFR-01",
+ "title": "Patient data confidentiality",
+ "description": "Records are accessible only to authorised roles.",
+ "priority": "must",
+ "rationale": "Clinical data carries regulatory obligations."
+ }
+ ],
+ "user_stories": [
+ {
+ "id": "US-01",
+ "as_a": "receptionist",
+ "i_want": "to book an appointment for a patient",
+ "so_that": "the patient is seen by the right doctor",
+ "acceptance_criteria": [
+ "Booking a free slot succeeds and returns a confirmation.",
+ "Booking an already-taken slot is rejected with a conflict error."
+ ],
+ "requirement_ids": [
+ "FR-02"
+ ],
+ "priority": "must"
+ }
+ ],
+ "out_of_scope": [
+ "Insurance claim submission"
+ ],
+ "open_questions": [
+ "Which regulatory regime applies to this deployment?"
+ ],
+ "reasoning": "The brief names patients, appointments, billing, doctors, and operations. I scoped the MVP to patient registration and appointment booking because billing depends on both and neither exists yet. Clinical data drove the confidentiality requirement; the regulatory regime is not stated, so I raised it as an open question rather than assuming one.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [],
+ "requires_approval": false,
+ "approval_reason": ""
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/qa_engineer.testing.json b/submissions/Victorious/apps/api/fixtures/qa_engineer.testing.json
new file mode 100644
index 00000000..cbad2f73
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/qa_engineer.testing.json
@@ -0,0 +1,54 @@
+{
+ "value": {
+ "strategy": "Cover booking conflicts first \u2014 the highest-risk behaviour.",
+ "test_cases": [
+ {
+ "id": "TC-01",
+ "title": "Double booking is rejected",
+ "given": "a slot already booked with a doctor",
+ "when": "a second booking is made for the same slot",
+ "then": "the request is rejected with a conflict error",
+ "kind": "integration",
+ "acceptance_criteria": "Booking an already-taken slot is rejected with a conflict error.",
+ "requirement_ids": [
+ "FR-02"
+ ]
+ }
+ ],
+ "coverage": [
+ {
+ "requirement_id": "FR-01",
+ "covered": false,
+ "test_case_ids": [],
+ "note": "No acceptance criteria were written for patient registration."
+ },
+ {
+ "requirement_id": "FR-02",
+ "covered": true,
+ "test_case_ids": [
+ "TC-01"
+ ],
+ "note": ""
+ }
+ ],
+ "defects": [
+ "The scaffold has no endpoint for FR-01 despite it being a must."
+ ],
+ "untestable": [
+ "NFR-01 names no roles, so authorisation cannot be tested."
+ ],
+ "reasoning": "Double booking is the highest-risk behaviour in the design, so it gets the first integration test. FR-01 has no acceptance criteria written against it, so I reported it uncovered rather than inventing an interpretation. The scaffold has no patient endpoint despite FR-01 being a must \u2014 that is a real defect, not speculation.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [
+ "FR-01 has no acceptance criteria, so patient registration cannot be verified against anything."
+ ],
+ "requires_approval": false,
+ "approval_reason": ""
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.acceptance_criteria.json b/submissions/Victorious/apps/api/fixtures/review.acceptance_criteria.json
new file mode 100644
index 00000000..f165588e
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.acceptance_criteria.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "Criteria are binary and traceable; FR-01 has none.",
+ "score_adjustment": -3,
+ "strengths": [
+ "Each criterion maps to a requirement id, so coverage is measurable."
+ ],
+ "weaknesses": [
+ "FR-01 (patient registration) has no acceptance criteria at all, which is why QA later reports it untestable."
+ ],
+ "suggestions": [
+ "Write criteria for FR-01 covering duplicate records and required fields."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.api_contract.json b/submissions/Victorious/apps/api/fixtures/review.api_contract.json
new file mode 100644
index 00000000..49b0b181
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.api_contract.json
@@ -0,0 +1,20 @@
+{
+ "value": {
+ "summary": "Correct HTTP semantics; the requirement set is only partly served.",
+ "score_adjustment": -4,
+ "strengths": [
+ "POST /api/v1/appointments uses the right verb and links to FR-02."
+ ],
+ "weaknesses": [
+ "No endpoint serves FR-01, so patient registration is unreachable through the API.",
+ "No error responses are described for the conflict path FR-02 requires."
+ ],
+ "suggestions": [
+ "Add the patient registration endpoint and document the 409 conflict response."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.api_documentation.json b/submissions/Victorious/apps/api/fixtures/review.api_documentation.json
new file mode 100644
index 00000000..7d32bc7f
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.api_documentation.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "Derived from the approved contract; inherits its gaps.",
+ "score_adjustment": -2,
+ "strengths": [
+ "Documents the endpoint that exists rather than one that does not."
+ ],
+ "weaknesses": [
+ "No error responses documented, so a client cannot handle the conflict path."
+ ],
+ "suggestions": [
+ "Document status codes, including the 409 the booking conflict produces."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.architecture_document.json b/submissions/Victorious/apps/api/fixtures/review.architecture_document.json
new file mode 100644
index 00000000..ab7ca9d7
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.architecture_document.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "Explains the reasoning rather than restating the component table.",
+ "score_adjustment": 4,
+ "strengths": [
+ "Explains why a modular monolith was chosen \u2014 the part a reader cannot recover from the code."
+ ],
+ "weaknesses": [
+ "Does not say what would have to change if the scale assumption proved wrong."
+ ],
+ "suggestions": [
+ "Record the signal that would justify splitting the monolith."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.business_analysis.json b/submissions/Victorious/apps/api/fixtures/review.business_analysis.json
new file mode 100644
index 00000000..90c326bf
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.business_analysis.json
@@ -0,0 +1,16 @@
+{
+ "value": {
+ "summary": "Genuine scrutiny \u2014 questions NFR-01 rather than validating everything.",
+ "score_adjustment": 4,
+ "strengths": [
+ "Questions NFR-01 instead of rubber-stamping it; an analyst that validates everything provides no signal.",
+ "The regulatory risk follows from the domain rather than being generic project risk."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.changelog.json b/submissions/Victorious/apps/api/fixtures/review.changelog.json
new file mode 100644
index 00000000..0b9489c5
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.changelog.json
@@ -0,0 +1,15 @@
+{
+ "value": {
+ "summary": "Factual and appropriately brief for an initial entry.",
+ "score_adjustment": 0,
+ "strengths": [
+ "States what was built without overstating completeness."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.coverage_report.json b/submissions/Victorious/apps/api/fixtures/review.coverage_report.json
new file mode 100644
index 00000000..68f3a674
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.coverage_report.json
@@ -0,0 +1,16 @@
+{
+ "value": {
+ "summary": "Reports the gap rather than hiding it behind a percentage.",
+ "score_adjustment": 5,
+ "strengths": [
+ "FR-01 is reported uncovered with the reason \u2014 no acceptance criteria \u2014 which is actionable.",
+ "Coverage is measured per requirement, so a gap is visible as a gap."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.database_schema.json b/submissions/Victorious/apps/api/fixtures/review.database_schema.json
new file mode 100644
index 00000000..1dc5a236
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.database_schema.json
@@ -0,0 +1,20 @@
+{
+ "value": {
+ "summary": "Entities and relationships are sound; indexing and retention are unaddressed.",
+ "score_adjustment": -2,
+ "strengths": [
+ "The patient-to-appointment relationship is stated explicitly."
+ ],
+ "weaknesses": [
+ "No index is defined for the slot lookup the booking conflict check depends on.",
+ "Nothing addresses retention for clinical data despite the regulatory risk on the register."
+ ],
+ "suggestions": [
+ "Add a uniqueness constraint on (doctor, slot) to enforce FR-02 in the schema."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.deployment_plan.json b/submissions/Victorious/apps/api/fixtures/review.deployment_plan.json
new file mode 100644
index 00000000..ac5b797e
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.deployment_plan.json
@@ -0,0 +1,17 @@
+{
+ "value": {
+ "summary": "Names what blocks production instead of implying readiness.",
+ "score_adjustment": 5,
+ "strengths": [
+ "`outstanding` names missing authentication and the FR-01 gap as real release blockers.",
+ "Environment variables are listed by name and purpose with no values.",
+ "The rollback note distinguishes what a redeploy cannot undo."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.developer_guide.json b/submissions/Victorious/apps/api/fixtures/review.developer_guide.json
new file mode 100644
index 00000000..ce3a905f
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.developer_guide.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "Covers setup; thin on the gotchas that matter most.",
+ "score_adjustment": -3,
+ "strengths": [
+ "Names the migration ordering requirement."
+ ],
+ "weaknesses": [
+ "No gotchas beyond migrations, and gotchas are the highest-value content in this document."
+ ],
+ "suggestions": [
+ "Document the booking conflict semantics, which are easy to implement wrongly."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.gap_analysis.json b/submissions/Victorious/apps/api/fixtures/review.gap_analysis.json
new file mode 100644
index 00000000..8dc9cc26
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.gap_analysis.json
@@ -0,0 +1,15 @@
+{
+ "value": {
+ "summary": "The access-control gap is correctly identified and correctly rated high.",
+ "score_adjustment": 0,
+ "strengths": [
+ "The recommendation is an action \u2014 define roles \u2014 not a restatement of the gap."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.implementation_plan.json b/submissions/Victorious/apps/api/fixtures/review.implementation_plan.json
new file mode 100644
index 00000000..6c14c713
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.implementation_plan.json
@@ -0,0 +1,20 @@
+{
+ "value": {
+ "summary": "Sequenced by risk \u2014 the schema first, the conflict logic second.",
+ "score_adjustment": 3,
+ "strengths": [
+ "T-02 tackles booking conflicts early rather than deferring the least certain work.",
+ "Dependencies are real and acyclic."
+ ],
+ "weaknesses": [
+ "No task covers the FR-01 endpoint the API contract also omits."
+ ],
+ "suggestions": [
+ "Add a task for patient registration so the gap does not reach implementation."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.prd.json b/submissions/Victorious/apps/api/fixtures/review.prd.json
new file mode 100644
index 00000000..774b4da2
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.prd.json
@@ -0,0 +1,21 @@
+{
+ "value": {
+ "summary": "Requirements are specific and prioritised; access control is named but not defined.",
+ "score_adjustment": 2,
+ "strengths": [
+ "FR-02 states the double-booking rejection explicitly \u2014 the case most likely to be implemented wrongly.",
+ "Every requirement carries a rationale, so a later reader can tell why it exists.",
+ "`out_of_scope` names insurance claims, which makes the MVP boundary checkable."
+ ],
+ "weaknesses": [
+ "NFR-01 requires confidentiality but names no roles or permission model, so two engineers would implement it differently."
+ ],
+ "suggestions": [
+ "Define the role set on NFR-01 before the architect designs authorisation from it."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.readme.json b/submissions/Victorious/apps/api/fixtures/review.readme.json
new file mode 100644
index 00000000..d6fe6dde
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.readme.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "Accurate about the scaffold; light on how to run it.",
+ "score_adjustment": 0,
+ "strengths": [
+ "States plainly that this is a scaffold, consistent with the engineer's own notes."
+ ],
+ "weaknesses": [
+ "No concrete setup commands, so a new reader cannot get started."
+ ],
+ "suggestions": [
+ "Add the exact commands to install dependencies and start the service."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.repository_structure.json b/submissions/Victorious/apps/api/fixtures/review.repository_structure.json
new file mode 100644
index 00000000..828b987b
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.repository_structure.json
@@ -0,0 +1,16 @@
+{
+ "value": {
+ "summary": "Honest about scope \u2014 names what it does not implement.",
+ "score_adjustment": 4,
+ "strengths": [
+ "`not_implemented` names authentication and migrations rather than leaving a reviewer to discover them.",
+ "The layout mirrors the approved components."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.risk_register.json b/submissions/Victorious/apps/api/fixtures/review.risk_register.json
new file mode 100644
index 00000000..dc084744
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.risk_register.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "Domain-specific risk with a real mitigation, though thin at one entry.",
+ "score_adjustment": -2,
+ "strengths": [
+ "The certification risk is specific to clinical data, not boilerplate schedule risk."
+ ],
+ "weaknesses": [
+ "A single risk under-represents a system handling patient data and payments."
+ ],
+ "suggestions": [
+ "Add data-retention and third-party-integration risks."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.source_file.json b/submissions/Victorious/apps/api/fixtures/review.source_file.json
new file mode 100644
index 00000000..87b61ab4
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.source_file.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "Realises the approved model; no validation or error handling.",
+ "score_adjustment": -3,
+ "strengths": [
+ "Field types match the approved schema."
+ ],
+ "weaknesses": [
+ "No validation on the fields NFR-01 implies should be constrained."
+ ],
+ "suggestions": [
+ "Add field validation and a docstring naming the requirement it serves."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.system_architecture.json b/submissions/Victorious/apps/api/fixtures/review.system_architecture.json
new file mode 100644
index 00000000..ba8751a2
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.system_architecture.json
@@ -0,0 +1,20 @@
+{
+ "value": {
+ "summary": "The modular-monolith choice is argued from the requirements rather than assumed.",
+ "score_adjustment": 5,
+ "strengths": [
+ "The style rationale reasons from team size and coupling instead of reaching for microservices.",
+ "Every component links to the requirement it serves, so nothing exists without a reason."
+ ],
+ "weaknesses": [
+ "Authorisation is listed as a security note but no component owns it, leaving NFR-01 unassigned."
+ ],
+ "suggestions": [
+ "Assign authorisation to a component or state that it is cross-cutting middleware."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.technology_decision.json b/submissions/Victorious/apps/api/fixtures/review.technology_decision.json
new file mode 100644
index 00000000..70c0b49c
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.technology_decision.json
@@ -0,0 +1,16 @@
+{
+ "value": {
+ "summary": "Reviewable: names the rejected alternative and the cost accepted.",
+ "score_adjustment": 6,
+ "strengths": [
+ "PostgreSQL over MongoDB is argued from transactional integrity across appointment tables.",
+ "The trade-off is stated \u2014 clinical notes need a JSONB column \u2014 so a human can approve it on its merits."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.test_cases.json b/submissions/Victorious/apps/api/fixtures/review.test_cases.json
new file mode 100644
index 00000000..c20c5538
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.test_cases.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "TC-01 is implementable as written and traced to its criterion.",
+ "score_adjustment": 0,
+ "strengths": [
+ "Given/when/then is concrete enough to implement without asking a question."
+ ],
+ "weaknesses": [
+ "Only one case, so most of the requirement set is unexercised."
+ ],
+ "suggestions": [
+ "Add cases for invalid input and unauthorised access."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.test_plan.json b/submissions/Victorious/apps/api/fixtures/review.test_plan.json
new file mode 100644
index 00000000..f4472579
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.test_plan.json
@@ -0,0 +1,16 @@
+{
+ "value": {
+ "summary": "Correctly targets the highest-risk behaviour first.",
+ "score_adjustment": 2,
+ "strengths": [
+ "The strategy names booking conflicts as the priority, matching where the design is least certain.",
+ "The scaffold defect it reports \u2014 no FR-01 endpoint \u2014 is real and verifiable."
+ ],
+ "weaknesses": [],
+ "suggestions": []
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/review.user_stories.json b/submissions/Victorious/apps/api/fixtures/review.user_stories.json
new file mode 100644
index 00000000..9984fd60
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/review.user_stories.json
@@ -0,0 +1,19 @@
+{
+ "value": {
+ "summary": "US-01 is testable end to end; coverage of the billing workflow is absent.",
+ "score_adjustment": 0,
+ "strengths": [
+ "US-01's acceptance criteria state both the success and the conflict path, so QA can write against them directly."
+ ],
+ "weaknesses": [
+ "Billing appears in the project brief but no story covers it, so it will not reach the architecture."
+ ],
+ "suggestions": [
+ "Add a billing story or record billing as explicitly deferred."
+ ]
+ },
+ "usage": {
+ "input_tokens": 1500,
+ "output_tokens": 320
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/software_architect.architecture.json b/submissions/Victorious/apps/api/fixtures/software_architect.architecture.json
new file mode 100644
index 00000000..59207e66
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/software_architect.architecture.json
@@ -0,0 +1,91 @@
+{
+ "value": {
+ "style": "Modular monolith",
+ "style_rationale": "One team, no independent scaling need; seams allow later split.",
+ "components": [
+ {
+ "name": "patients",
+ "responsibility": "Owns patient records and demographics.",
+ "depends_on": [],
+ "requirement_ids": [
+ "FR-01"
+ ]
+ },
+ {
+ "name": "scheduling",
+ "responsibility": "Owns appointments and slot availability.",
+ "depends_on": [
+ "patients"
+ ],
+ "requirement_ids": [
+ "FR-02"
+ ]
+ }
+ ],
+ "technology_choices": [
+ {
+ "layer": "database",
+ "choice": "PostgreSQL",
+ "alternatives": [
+ "MongoDB"
+ ],
+ "rationale": "Appointments need transactional integrity across tables.",
+ "tradeoffs": "Flexible clinical notes need a JSONB column."
+ }
+ ],
+ "api_endpoints": [
+ {
+ "method": "POST",
+ "path": "/api/v1/appointments",
+ "purpose": "Book an appointment.",
+ "request_summary": "patient_id, doctor_id, slot",
+ "response_summary": "appointment with confirmation code",
+ "requirement_ids": [
+ "FR-02"
+ ]
+ }
+ ],
+ "data_entities": [
+ {
+ "name": "patient",
+ "purpose": "A person receiving care.",
+ "fields": [
+ {
+ "name": "id",
+ "type": "uuid",
+ "nullable": false,
+ "description": "PK"
+ },
+ {
+ "name": "name",
+ "type": "text",
+ "nullable": false,
+ "description": ""
+ }
+ ],
+ "relationships": [
+ "one-to-many with appointment"
+ ]
+ }
+ ],
+ "scalability_notes": [
+ "Single instance is sufficient at the stated scale."
+ ],
+ "security_notes": [
+ "Role-based access on every patient-scoped endpoint."
+ ],
+ "reasoning": "One team, no independent scaling requirement, and two closely coupled domains: a modular monolith with clean seams is correct here, and distribution would be a cost with no matching benefit. PostgreSQL over MongoDB because appointments need transactional integrity across tables; the cost is that flexible clinical notes need a JSONB column.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [
+ "NFR-01 still names no roles, so the access-control design rests on an assumption rather than a stated requirement."
+ ],
+ "requires_approval": true,
+ "approval_reason": "Technology selection commits the project to PostgreSQL."
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/fixtures/software_architect.development_planning.json b/submissions/Victorious/apps/api/fixtures/software_architect.development_planning.json
new file mode 100644
index 00000000..7df7d251
--- /dev/null
+++ b/submissions/Victorious/apps/api/fixtures/software_architect.development_planning.json
@@ -0,0 +1,45 @@
+{
+ "value": {
+ "sequencing_rationale": "Data model first; scheduling conflict logic is riskiest.",
+ "tasks": [
+ {
+ "id": "T-01",
+ "title": "Patient schema and migrations",
+ "description": "Create the patient table and its migration.",
+ "component": "patients",
+ "depends_on": [],
+ "requirement_ids": [
+ "FR-01"
+ ],
+ "estimate": "half a day"
+ },
+ {
+ "id": "T-02",
+ "title": "Appointment booking with conflict rejection",
+ "description": "Booking endpoint that rejects double-booked slots.",
+ "component": "scheduling",
+ "depends_on": [
+ "T-01"
+ ],
+ "requirement_ids": [
+ "FR-02"
+ ],
+ "estimate": "one day"
+ }
+ ],
+ "milestones": [
+ "Appointments can be booked and listed through the API."
+ ],
+ "reasoning": "The patient schema comes first because everything references it, and the booking conflict logic second because it is the least certain part of the design. Deferring it would mean discovering a design problem at the point where changing it is most expensive.",
+ "confidence": 0.86,
+ "sources": "$upstream",
+ "artifacts": [],
+ "concerns": [],
+ "requires_approval": false,
+ "approval_reason": ""
+ },
+ "usage": {
+ "input_tokens": 2400,
+ "output_tokens": 1800
+ }
+}
diff --git a/submissions/Victorious/apps/api/pyproject.toml b/submissions/Victorious/apps/api/pyproject.toml
new file mode 100644
index 00000000..8ac122e4
--- /dev/null
+++ b/submissions/Victorious/apps/api/pyproject.toml
@@ -0,0 +1,63 @@
+[project]
+name = "victorious-api"
+version = "0.1.0"
+description = "Project Victorious — AI-native Software Engineering Organization (API)"
+requires-python = ">=3.12"
+dependencies = [
+ "fastapi>=0.115.0",
+ "uvicorn[standard]>=0.32.0",
+ "pydantic>=2.9.0",
+ "pydantic-settings>=2.6.0",
+ "sqlalchemy[asyncio]>=2.0.36",
+ "alembic>=1.14.0",
+ "aiosqlite>=0.20.0",
+ "asyncpg>=0.30.0",
+ "anthropic>=0.40.0",
+ "google-genai>=0.3.0",
+ "langgraph>=1.0.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=8.3.0",
+ "pytest-asyncio>=0.24.0",
+ "httpx>=0.27.0",
+ "ruff>=0.7.0",
+ "mypy>=1.13.0",
+]
+
+[build-system]
+requires = ["setuptools>=75"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+include = ["app*"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+asyncio_mode = "auto"
+filterwarnings = ["error"]
+
+[tool.ruff]
+target-version = "py312"
+line-length = 100
+
+[tool.ruff.lint]
+# BLE (blind except) and S (bandit security) are enabled deliberately: broad
+# excepts and unguarded asserts are exactly the shortcuts that creep in under
+# deadline pressure. Where one is genuinely correct it carries an explaining noqa.
+select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "SIM", "RUF", "BLE", "S"]
+
+[tool.ruff.lint.per-file-ignores]
+# Asserts are the point of a test suite.
+"tests/**" = ["S101"]
+
+[tool.mypy]
+python_version = "3.12"
+strict = true
+warn_unreachable = true
+plugins = []
+
+[[tool.mypy.overrides]]
+module = "tests.*"
+disallow_untyped_defs = false
diff --git a/submissions/Victorious/apps/api/scripts/generate_fixtures.py b/submissions/Victorious/apps/api/scripts/generate_fixtures.py
new file mode 100644
index 00000000..c8a4db9f
--- /dev/null
+++ b/submissions/Victorious/apps/api/scripts/generate_fixtures.py
@@ -0,0 +1,232 @@
+"""Writes the demo fixture corpus.
+
+The organization needs recorded reasoning to run without a provider (ADR-0008).
+This generates that corpus from the worked payloads in
+``tests/test_organization.py`` — the same data the end-to-end test verifies —
+so the offline demo and the test suite cannot drift apart.
+
+Once a live provider has been run with ``VICTORIOUS_LLM__RECORD_FIXTURES=true``,
+those recordings replace these and this script becomes a fallback.
+
+Run from ``apps/api``::
+
+ python scripts/generate_fixtures.py
+"""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(ROOT))
+sys.path.insert(0, str(ROOT / "tests"))
+
+from review_fixtures import REVIEW_JUDGEMENTS # noqa: E402
+from test_organization import PAYLOADS # noqa: E402
+
+from app.llm.fixture_provider import UPSTREAM_TOKEN # noqa: E402
+
+FIXTURE_DIR = ROOT / "fixtures"
+
+#: Written by the Executive AI when it raises an approval gate. Keyed by gate
+#: kind, matching `ExecutiveAI._narrate`'s fixture key.
+GATE_NARRATIONS: dict[str, dict[str, str]] = {
+ "requirements": {
+ "title": "Approve the requirements before the architecture is designed",
+ "what_changed": (
+ "The Product Manager defined the functional and non-functional "
+ "requirements, user stories, and acceptance criteria. The Business "
+ "Analyst validated them and flagged access control as underspecified."
+ ),
+ "why": (
+ "Everything the architect designs derives from these requirements. "
+ "Approving them here prevents a design being built on scope you have "
+ "not reviewed."
+ ),
+ },
+ "architecture": {
+ "title": "Approve the architecture before work is planned against it",
+ "what_changed": (
+ "The Software Architect proposed a modular monolith with patients and "
+ "scheduling components, selected PostgreSQL over MongoDB, and defined "
+ "the API contract and data model."
+ ),
+ "why": (
+ "The implementation plan and the generated scaffold both derive from "
+ "this design. The technology selection in particular is expensive to "
+ "reverse once code exists."
+ ),
+ },
+ "technology_selection": {
+ "title": "Approve the technology selection",
+ "what_changed": (
+ "The Software Architect selected PostgreSQL over MongoDB, recording "
+ "the alternative considered and the trade-off accepted."
+ ),
+ "why": (
+ "The architect judged this expensive to reverse once code exists and "
+ "raised it for review itself. The schema, the API contract, and the "
+ "generated scaffold all assume it."
+ ),
+ },
+ "resynchronisation": {
+ "title": "Approve re-synchronisation of work that is now out of date",
+ "what_changed": (
+ "Upstream work was revised after the artifacts below were derived "
+ "from it, so those artifacts no longer reflect the current version."
+ ),
+ "why": (
+ "Approving reruns the affected specialists against the revised "
+ "upstream. Declining leaves the work in place with its staleness "
+ "still visible, so nothing is silently rebuilt."
+ ),
+ },
+ "code_generation": {
+ "title": "Authorise code generation",
+ "what_changed": (
+ "The implementation plan sequences the approved architecture into "
+ "dependency-ordered tasks, starting with the data model and the "
+ "booking conflict logic."
+ ),
+ "why": (
+ "This is the last gate before the organization writes the repository "
+ "scaffold. Everything generated will trace back to this plan."
+ ),
+ },
+}
+
+
+def main() -> int:
+ FIXTURE_DIR.mkdir(parents=True, exist_ok=True)
+ written = 0
+
+ for key, payload in PAYLOADS.items():
+ value = {
+ **payload,
+ "reasoning": _reasoning_for(key),
+ "confidence": 0.86,
+ # Expanded at replay time to the artifact IDs actually present in the
+ # agent's context — artifact IDs are per-project, so a recording
+ # cannot name them.
+ "sources": UPSTREAM_TOKEN,
+ "artifacts": [],
+ "concerns": _concerns_for(key),
+ "requires_approval": key == "software_architect.architecture",
+ "approval_reason": (
+ "Technology selection commits the project to PostgreSQL."
+ if key == "software_architect.architecture"
+ else ""
+ ),
+ }
+ _write(
+ f"{key}.json",
+ {"value": value, "usage": {"input_tokens": 2400, "output_tokens": 1800}},
+ )
+ written += 1
+
+ # One recorded judgement per artifact type, so the review layer produces real
+ # prose and a real score spread offline (ADR-0008).
+ for artifact_type, judgement in REVIEW_JUDGEMENTS.items():
+ _write(
+ f"review.{artifact_type}.json",
+ {"value": judgement, "usage": {"input_tokens": 1500, "output_tokens": 320}},
+ )
+ written += 1
+
+ for gate, narration in GATE_NARRATIONS.items():
+ _write(
+ f"executive.gate.{gate}.json",
+ {"value": narration, "usage": {"input_tokens": 900, "output_tokens": 220}},
+ )
+ written += 1
+
+ print(f"Wrote {written} fixtures to {FIXTURE_DIR}")
+ return 0
+
+
+def _reasoning_for(key: str) -> str:
+ reasoning = {
+ "product_manager.requirement_discovery": (
+ "The brief names patients, appointments, billing, doctors, and operations. "
+ "I scoped the MVP to patient registration and appointment booking because "
+ "billing depends on both and neither exists yet. Clinical data drove the "
+ "confidentiality requirement; the regulatory regime is not stated, so I "
+ "raised it as an open question rather than assuming one."
+ ),
+ "business_analyst.business_validation": (
+ "FR-01 and FR-02 are specific and testable. NFR-01 names no roles and no "
+ "permission model, so two engineers would implement it differently — I "
+ "questioned it rather than letting the architect guess. The regulatory "
+ "exposure follows from the domain and belongs on the risk register."
+ ),
+ "software_architect.architecture": (
+ "One team, no independent scaling requirement, and two closely coupled "
+ "domains: a modular monolith with clean seams is correct here, and "
+ "distribution would be a cost with no matching benefit. PostgreSQL over "
+ "MongoDB because appointments need transactional integrity across tables; "
+ "the cost is that flexible clinical notes need a JSONB column."
+ ),
+ "software_architect.development_planning": (
+ "The patient schema comes first because everything references it, and the "
+ "booking conflict logic second because it is the least certain part of the "
+ "design. Deferring it would mean discovering a design problem at the point "
+ "where changing it is most expensive."
+ ),
+ "full_stack_engineer.implementation": (
+ "I wrote the patient model because it is where the approved data design "
+ "becomes concrete, and left package manifests and lint configuration to the "
+ "tree. Authentication and migrations are genuinely absent and recorded as "
+ "such — this is a scaffold, not a running system."
+ ),
+ "qa_engineer.testing": (
+ "Double booking is the highest-risk behaviour in the design, so it gets the "
+ "first integration test. FR-01 has no acceptance criteria written against "
+ "it, so I reported it uncovered rather than inventing an interpretation. "
+ "The scaffold has no patient endpoint despite FR-01 being a must — that is "
+ "a real defect, not speculation."
+ ),
+ "documentation.documentation": (
+ "I documented what the organization actually decided: the modular monolith "
+ "and why, the PostgreSQL trade-off, and the endpoints in the approved "
+ "contract. The README states that this is a scaffold, because the QA "
+ "coverage report and the engineer's own notes both say so."
+ ),
+ "documentation.deployment_preparation": (
+ "The plan follows the approved PostgreSQL and container decisions and "
+ "introduces no infrastructure the organization did not choose. Environment "
+ "variables are listed by name and purpose only. Authentication being absent "
+ "and FR-01 having no endpoint genuinely block a production release."
+ ),
+ }
+ return reasoning.get(key, "Completed this stage from the upstream context provided.")
+
+
+def _concerns_for(key: str) -> list[str]:
+ """Concerns raised about upstream work.
+
+ Populated for the stages where the specification expects an agent to push
+ back, so the demo shows cross-validation actually happening rather than every
+ agent silently agreeing.
+ """
+ concerns = {
+ "software_architect.architecture": [
+ "NFR-01 still names no roles, so the access-control design rests on an "
+ "assumption rather than a stated requirement."
+ ],
+ "qa_engineer.testing": [
+ "FR-01 has no acceptance criteria, so patient registration cannot be "
+ "verified against anything."
+ ],
+ }
+ return concerns.get(key, [])
+
+
+def _write(name: str, payload: dict[str, object]) -> None:
+ path = FIXTURE_DIR / name
+ path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/submissions/Victorious/apps/api/scripts/review_fixtures.py b/submissions/Victorious/apps/api/scripts/review_fixtures.py
new file mode 100644
index 00000000..760b451e
--- /dev/null
+++ b/submissions/Victorious/apps/api/scripts/review_fixtures.py
@@ -0,0 +1,270 @@
+"""Recorded review judgements, one per artifact type.
+
+Written by ``generate_fixtures.py`` so the review layer produces real prose and a
+real score spread with no network (ADR-0008).
+
+Each entry names something concrete in the artifact it reviews — `FR-01`, the
+PostgreSQL-over-MongoDB trade-off, `TC-01` — because a finding that could apply
+to any project is not a finding. The adjustments are mostly small or zero: the
+structural score already reflects most artifacts, and a reviewer that nudges
+every score teaches the reader to ignore the number.
+
+Replaced wholesale the first time the reviewer runs against a live provider with
+``VICTORIOUS_LLM__RECORD_FIXTURES=true``.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+REVIEW_JUDGEMENTS: dict[str, dict[str, Any]] = {
+ "prd": {
+ "summary": (
+ "Requirements are specific and prioritised; access control is named but not defined."
+ ),
+ "score_adjustment": 2,
+ "strengths": [
+ "FR-02 states the double-booking rejection explicitly — the case most likely to be "
+ "implemented wrongly.",
+ "Every requirement carries a rationale, so a later reader can tell why it exists.",
+ "`out_of_scope` names insurance claims, which makes the MVP boundary checkable.",
+ ],
+ "weaknesses": [
+ "NFR-01 requires confidentiality but names no roles or permission model, so two "
+ "engineers would implement it differently.",
+ ],
+ "suggestions": [
+ "Define the role set on NFR-01 before the architect designs authorisation from it.",
+ ],
+ },
+ "user_stories": {
+ "summary": "US-01 is testable end to end; coverage of the billing workflow is absent.",
+ "score_adjustment": 0,
+ "strengths": [
+ "US-01's acceptance criteria state both the success and the conflict path, so QA can "
+ "write against them directly.",
+ ],
+ "weaknesses": [
+ "Billing appears in the project brief but no story covers it, so it will not reach the "
+ "architecture.",
+ ],
+ "suggestions": ["Add a billing story or record billing as explicitly deferred."],
+ },
+ "acceptance_criteria": {
+ "summary": "Criteria are binary and traceable; FR-01 has none.",
+ "score_adjustment": -3,
+ "strengths": ["Each criterion maps to a requirement id, so coverage is measurable."],
+ "weaknesses": [
+ "FR-01 (patient registration) has no acceptance criteria at all, which is why QA later "
+ "reports it untestable.",
+ ],
+ "suggestions": ["Write criteria for FR-01 covering duplicate records and required fields."],
+ },
+ "business_analysis": {
+ "summary": "Genuine scrutiny — questions NFR-01 rather than validating everything.",
+ "score_adjustment": 4,
+ "strengths": [
+ "Questions NFR-01 instead of rubber-stamping it; an analyst that validates everything "
+ "provides no signal.",
+ "The regulatory risk follows from the domain rather than being generic project risk.",
+ ],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+ "gap_analysis": {
+ "summary": "The access-control gap is correctly identified and correctly rated high.",
+ "score_adjustment": 0,
+ "strengths": [
+ "The recommendation is an action — define roles — not a restatement of the gap."
+ ],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+ "risk_register": {
+ "summary": "Domain-specific risk with a real mitigation, though thin at one entry.",
+ "score_adjustment": -2,
+ "strengths": [
+ "The certification risk is specific to clinical data, not boilerplate schedule risk."
+ ],
+ "weaknesses": [
+ "A single risk under-represents a system handling patient data and payments."
+ ],
+ "suggestions": ["Add data-retention and third-party-integration risks."],
+ },
+ "system_architecture": {
+ "summary": (
+ "The modular-monolith choice is argued from the requirements rather than assumed."
+ ),
+ "score_adjustment": 5,
+ "strengths": [
+ "The style rationale reasons from team size and coupling instead of reaching for "
+ "microservices.",
+ "Every component links to the requirement it serves, so nothing exists without a "
+ "reason.",
+ ],
+ "weaknesses": [
+ "Authorisation is listed as a security note but no component owns it, leaving NFR-01 "
+ "unassigned.",
+ ],
+ "suggestions": [
+ "Assign authorisation to a component or state that it is cross-cutting middleware."
+ ],
+ },
+ "technology_decision": {
+ "summary": "Reviewable: names the rejected alternative and the cost accepted.",
+ "score_adjustment": 6,
+ "strengths": [
+ "PostgreSQL over MongoDB is argued from transactional integrity across appointment "
+ "tables.",
+ "The trade-off is stated — clinical notes need a JSONB column — so a human can approve "
+ "it on its merits.",
+ ],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+ "api_contract": {
+ "summary": "Correct HTTP semantics; the requirement set is only partly served.",
+ "score_adjustment": -4,
+ "strengths": ["POST /api/v1/appointments uses the right verb and links to FR-02."],
+ "weaknesses": [
+ "No endpoint serves FR-01, so patient registration is unreachable through the API.",
+ "No error responses are described for the conflict path FR-02 requires.",
+ ],
+ "suggestions": [
+ "Add the patient registration endpoint and document the 409 conflict response.",
+ ],
+ },
+ "database_schema": {
+ "summary": "Entities and relationships are sound; indexing and retention are unaddressed.",
+ "score_adjustment": -2,
+ "strengths": ["The patient-to-appointment relationship is stated explicitly."],
+ "weaknesses": [
+ "No index is defined for the slot lookup the booking conflict check depends on.",
+ "Nothing addresses retention for clinical data despite the regulatory risk on the "
+ "register.",
+ ],
+ "suggestions": [
+ "Add a uniqueness constraint on (doctor, slot) to enforce FR-02 in the schema."
+ ],
+ },
+ "implementation_plan": {
+ "summary": "Sequenced by risk — the schema first, the conflict logic second.",
+ "score_adjustment": 3,
+ "strengths": [
+ "T-02 tackles booking conflicts early rather than deferring the least certain work.",
+ "Dependencies are real and acyclic.",
+ ],
+ "weaknesses": ["No task covers the FR-01 endpoint the API contract also omits."],
+ "suggestions": [
+ "Add a task for patient registration so the gap does not reach implementation."
+ ],
+ },
+ "repository_structure": {
+ "summary": "Honest about scope — names what it does not implement.",
+ "score_adjustment": 4,
+ "strengths": [
+ "`not_implemented` names authentication and migrations rather than leaving a reviewer "
+ "to discover them.",
+ "The layout mirrors the approved components.",
+ ],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+ "source_file": {
+ "summary": "Realises the approved model; no validation or error handling.",
+ "score_adjustment": -3,
+ "strengths": ["Field types match the approved schema."],
+ "weaknesses": ["No validation on the fields NFR-01 implies should be constrained."],
+ "suggestions": ["Add field validation and a docstring naming the requirement it serves."],
+ },
+ "test_plan": {
+ "summary": "Correctly targets the highest-risk behaviour first.",
+ "score_adjustment": 2,
+ "strengths": [
+ "The strategy names booking conflicts as the priority, matching where the design is "
+ "least certain.",
+ "The scaffold defect it reports — no FR-01 endpoint — is real and verifiable.",
+ ],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+ "test_cases": {
+ "summary": "TC-01 is implementable as written and traced to its criterion.",
+ "score_adjustment": 0,
+ "strengths": ["Given/when/then is concrete enough to implement without asking a question."],
+ "weaknesses": ["Only one case, so most of the requirement set is unexercised."],
+ "suggestions": ["Add cases for invalid input and unauthorised access."],
+ },
+ "coverage_report": {
+ "summary": "Reports the gap rather than hiding it behind a percentage.",
+ "score_adjustment": 5,
+ "strengths": [
+ "FR-01 is reported uncovered with the reason — no acceptance criteria — which is "
+ "actionable.",
+ "Coverage is measured per requirement, so a gap is visible as a gap.",
+ ],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+ "readme": {
+ "summary": "Accurate about the scaffold; light on how to run it.",
+ "score_adjustment": 0,
+ "strengths": [
+ "States plainly that this is a scaffold, consistent with the engineer's own notes."
+ ],
+ "weaknesses": ["No concrete setup commands, so a new reader cannot get started."],
+ "suggestions": ["Add the exact commands to install dependencies and start the service."],
+ },
+ "api_documentation": {
+ "summary": "Derived from the approved contract; inherits its gaps.",
+ "score_adjustment": -2,
+ "strengths": ["Documents the endpoint that exists rather than one that does not."],
+ "weaknesses": [
+ "No error responses documented, so a client cannot handle the conflict path."
+ ],
+ "suggestions": ["Document status codes, including the 409 the booking conflict produces."],
+ },
+ "architecture_document": {
+ "summary": "Explains the reasoning rather than restating the component table.",
+ "score_adjustment": 4,
+ "strengths": [
+ "Explains why a modular monolith was chosen — the part a reader cannot "
+ "recover from the code.",
+ ],
+ "weaknesses": [
+ "Does not say what would have to change if the scale assumption proved wrong."
+ ],
+ "suggestions": ["Record the signal that would justify splitting the monolith."],
+ },
+ "developer_guide": {
+ "summary": "Covers setup; thin on the gotchas that matter most.",
+ "score_adjustment": -3,
+ "strengths": ["Names the migration ordering requirement."],
+ "weaknesses": [
+ "No gotchas beyond migrations, and gotchas are the highest-value content in this "
+ "document.",
+ ],
+ "suggestions": [
+ "Document the booking conflict semantics, which are easy to implement wrongly."
+ ],
+ },
+ "changelog": {
+ "summary": "Factual and appropriately brief for an initial entry.",
+ "score_adjustment": 0,
+ "strengths": ["States what was built without overstating completeness."],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+ "deployment_plan": {
+ "summary": "Names what blocks production instead of implying readiness.",
+ "score_adjustment": 5,
+ "strengths": [
+ "`outstanding` names missing authentication and the FR-01 gap as real "
+ "release blockers.",
+ "Environment variables are listed by name and purpose with no values.",
+ "The rollback note distinguishes what a redeploy cannot undo.",
+ ],
+ "weaknesses": [],
+ "suggestions": [],
+ },
+}
diff --git a/submissions/Victorious/apps/api/scripts/seed_demo.py b/submissions/Victorious/apps/api/scripts/seed_demo.py
new file mode 100644
index 00000000..5c3c66f9
--- /dev/null
+++ b/submissions/Victorious/apps/api/scripts/seed_demo.py
@@ -0,0 +1,172 @@
+"""Seeds the demonstration project.
+
+`13_Demo_and_Pitch.md` requires a polished end-to-end demonstration, and
+`12_Risk_Analysis.md` rates Model Availability a Medium risk. A demo that opens
+on an empty dashboard and depends on a live provider to have anything to show is
+one network problem away from failing in front of judges.
+
+This drives the hospital scenario from `13_Demo_and_Pitch.md` through the full
+lifecycle on recorded fixtures, so every view has real content the moment the
+workspace opens — and the presenter can still create a second project live to
+show the organization working.
+
+Run from ``apps/api``::
+
+ .venv/Scripts/python scripts/seed_demo.py # completed project
+ .venv/Scripts/python scripts/seed_demo.py --at-gate # stopped at the first gate
+ .venv/Scripts/python scripts/seed_demo.py --reset # wipe and reseed
+"""
+
+from __future__ import annotations
+
+import argparse
+import asyncio
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(ROOT))
+
+from app.core.bootstrap import build_container # noqa: E402
+from app.core.config import LLMProvider, LLMSettings, Settings, get_settings # noqa: E402
+from app.db.session import Database # noqa: E402
+from app.domain.approvals import ApprovalStatus # noqa: E402
+from app.domain.projects import Project # noqa: E402
+from app.memory.repository import SharedMemory # noqa: E402
+from app.orchestration.runner import OrchestrationRunner # noqa: E402
+
+DEMO_NAME = "Hospital Management System"
+DEMO_DESCRIPTION = (
+ "A platform for managing patients, appointments, billing, doctors, "
+ "and hospital operations."
+)
+
+#: A lifecycle that needs more passes than this is not converging, and a demo
+#: seeded from a hung workflow is worse than no seed.
+MAX_PASSES = 14
+
+
+async def seed(*, stop_at_gate: bool, reset: bool) -> int:
+ settings = _settings()
+ container = build_container(settings)
+ database = container.resolve(Database)
+
+ await database.create_schema()
+
+ memory: SharedMemory = container.resolve(SharedMemory) # type: ignore[type-abstract]
+ runner: OrchestrationRunner = container.resolve(OrchestrationRunner)
+
+ try:
+ existing = [
+ project
+ for project in await memory.projects.list_all(limit=100)
+ if project.name == DEMO_NAME
+ ]
+
+ if existing and not reset:
+ print(f"Demo project already seeded: {existing[0].id}")
+ print("Pass --reset to wipe and reseed.")
+ return 0
+
+ if existing and reset:
+ # Deleting is out of scope for the memory protocol — this is a demo
+ # helper, not a data-management feature. Removing the database file
+ # is the honest way to reset, and the caller is told so.
+ print("Reset requested. Delete apps/api/victorious.db and run again.")
+ return 1
+
+ project = await memory.projects.create(
+ Project(name=DEMO_NAME, description=DEMO_DESCRIPTION)
+ )
+ print(f"Created {project.name} ({project.id})")
+
+ for pass_number in range(1, MAX_PASSES + 1):
+ outcome = await runner.advance(project.id)
+
+ if outcome.executed_stages:
+ stages = ", ".join(stage.value for stage in outcome.executed_stages)
+ print(f" pass {pass_number}: {stages}")
+
+ if outcome.is_complete:
+ break
+
+ if outcome.is_blocked:
+ print(f" blocked: {outcome.halt_reason}")
+ return 1
+
+ if outcome.awaiting_approval:
+ pending = await memory.approvals.list_for_project(
+ project.id, pending_only=True
+ )
+
+ if stop_at_gate:
+ print(f" stopped at gate: {pending[0].title}")
+ break
+
+ for request in pending:
+ await runner.executive.record_decision(
+ request.id, ApprovalStatus.APPROVED, None
+ )
+ print(f" approved: {request.kind.value}")
+
+ await _report(memory, project.id)
+ return 0
+
+ finally:
+ await container.aclose()
+
+
+async def _report(memory: SharedMemory, project_id: str) -> None:
+ project = await memory.projects.get(project_id)
+ artifacts = [a for a in await memory.artifacts.list_for_project(project_id) if a.has_content]
+ edges = await memory.traces.list_for_project(project_id)
+ runs = await memory.runs.list_for_project(project_id)
+ approvals = await memory.approvals.list_for_project(project_id)
+ events = await memory.events.list_for_project(project_id, limit=1000)
+
+ print()
+ print(f" stages {len(project.completed_stages)}/8 complete")
+ print(f" artifacts {len(artifacts)}")
+ print(f" edges {len(edges)}")
+ print(f" agent runs {len(runs)}")
+ print(f" approvals {len(approvals)}")
+ print(f" events {len(events)}")
+ print()
+ print(f"Open http://localhost:3000/projects/{project_id}")
+
+
+def _settings() -> Settings:
+ """Demo settings: recorded fixtures, so seeding needs no network.
+
+ The configured provider is deliberately overridden rather than inherited. A
+ seed that quietly spent API credits — or failed because a key was missing —
+ would defeat the purpose of having a seed at all.
+ """
+ base = get_settings()
+ return base.model_copy(
+ update={
+ "llm": LLMSettings(
+ provider=LLMProvider.FIXTURE,
+ fixture_dir=str(ROOT / "fixtures"),
+ )
+ }
+ )
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description="Seed the demonstration project.")
+ parser.add_argument(
+ "--at-gate",
+ action="store_true",
+ help="Stop at the first approval gate, so the demo opens on a decision.",
+ )
+ parser.add_argument(
+ "--reset", action="store_true", help="Explain how to wipe and reseed."
+ )
+ args = parser.parse_args()
+
+ return asyncio.run(seed(stop_at_gate=args.at_gate, reset=args.reset))
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/submissions/Victorious/apps/api/tests/conftest.py b/submissions/Victorious/apps/api/tests/conftest.py
new file mode 100644
index 00000000..4be73944
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/conftest.py
@@ -0,0 +1,52 @@
+"""Shared test fixtures."""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+
+import pytest
+from httpx import ASGITransport, AsyncClient
+
+from app.core.config import (
+ DatabaseSettings,
+ Environment,
+ LLMProvider,
+ LLMSettings,
+ ObservabilitySettings,
+ Settings,
+)
+from app.main import create_app
+
+
+@pytest.fixture
+def settings() -> Settings:
+ """Isolated test configuration.
+
+ Constructed explicitly rather than read from the environment so the suite is
+ deterministic regardless of the developer's shell. Uses an in-memory database
+ and the fixture LLM provider: tests must never make a network call.
+ """
+ return Settings(
+ environment=Environment.TEST,
+ database=DatabaseSettings(url="sqlite+aiosqlite:///:memory:"),
+ llm=LLMSettings(provider=LLMProvider.FIXTURE),
+ observability=ObservabilitySettings(log_level="WARNING", json_logs=False),
+ )
+
+
+@pytest.fixture
+async def client(settings: Settings) -> AsyncIterator[AsyncClient]:
+ """HTTP client bound to the app in-process.
+
+ ``ASGITransport`` exercises the real middleware and handler stack without
+ binding a socket.
+ """
+ app = create_app(settings)
+
+ # The lifespan context is entered alongside the client so the container is
+ # built exactly as it is in production, rather than stubbed for tests.
+ async with (
+ AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as async_client,
+ app.router.lifespan_context(app),
+ ):
+ yield async_client
diff --git a/submissions/Victorious/apps/api/tests/test_agent_base.py b/submissions/Victorious/apps/api/tests/test_agent_base.py
new file mode 100644
index 00000000..684fde2a
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_agent_base.py
@@ -0,0 +1,424 @@
+"""Agent framework: execution template, orphan guard, and provider portability."""
+
+from __future__ import annotations
+
+import json
+from collections.abc import AsyncIterator
+from pathlib import Path
+
+import pytest
+import pytest_asyncio
+from pydantic import BaseModel, Field
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput
+from app.agents.prompts import PromptError, available_prompts, load_prompt, render_prompt
+from app.core.config import DatabaseSettings
+from app.db.session import Database
+from app.domain.agents import AgentRunStatus, TokenUsage
+from app.domain.artifacts import Artifact, ArtifactType, ArtifactVersion
+from app.domain.errors import ProviderError, ValidationError
+from app.domain.events import EventType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.projects import Project
+from app.domain.traceability import TraceKind
+from app.events.bus import EventBus
+from app.llm.fixture_provider import FixtureProvider
+from app.llm.provider import CompletionRequest, CompletionResponse, StructuredResponse
+from app.memory.context_builder import ContextBuilder, ProjectContext
+from app.memory.sql_repository import SqlSharedMemory
+
+
+class ArchitectOutput(AgentOutput):
+ """A minimal agent contract for exercising the framework."""
+
+ recommended_stack: list[str] = Field(default_factory=list)
+
+
+class SampleAgent(BaseAgent[ArchitectOutput]):
+ role = AgentRole.SOFTWARE_ARCHITECT
+ stage = LifecycleStage.ARCHITECTURE
+ output_model = ArchitectOutput
+ prompt_name = "engineering_organization"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return f"Design the architecture using {len(context.entries)} upstream artifact(s)."
+
+
+class ScriptedProvider:
+ """Returns a prepared output, recording what it was asked."""
+
+ name = "scripted"
+ model = "scripted-1"
+
+ def __init__(self, payload: dict[str, object]) -> None:
+ self._payload = payload
+ self.requests: list[CompletionRequest] = []
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ self.requests.append(request)
+ return CompletionResponse(
+ text="", usage=TokenUsage(), model=self.model, provider=self.name
+ )
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ self.requests.append(request)
+ return StructuredResponse(
+ value=schema.model_validate(self._payload),
+ raw_json=json.dumps(self._payload),
+ usage=TokenUsage(input_tokens=100, output_tokens=200),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ yield ""
+
+ async def aclose(self) -> None:
+ return None
+
+
+class ExplodingProvider(ScriptedProvider):
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ raise ProviderError("upstream model unavailable")
+
+
+@pytest_asyncio.fixture
+async def memory() -> AsyncIterator[SqlSharedMemory]:
+ database = Database(
+ DatabaseSettings(url="sqlite+aiosqlite:///file:agentdb?mode=memory&cache=shared&uri=true")
+ )
+ await database.create_schema()
+ try:
+ yield SqlSharedMemory(database)
+ finally:
+ await database.aclose()
+
+
+@pytest_asyncio.fixture
+async def project(memory: SqlSharedMemory) -> Project:
+ return await memory.projects.create(
+ Project(name="Hospital System", description="Patients, appointments, billing.")
+ )
+
+
+async def add_upstream(memory: SqlSharedMemory, project: Project) -> Artifact:
+ """Create an approved requirements artifact in an upstream stage."""
+ artifact = await memory.artifacts.create(
+ Artifact(
+ project_id=project.id,
+ type=ArtifactType.PRD,
+ title="Product Requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ owner_role=AgentRole.PRODUCT_MANAGER,
+ )
+ )
+ await memory.artifacts.append_version(
+ artifact.id,
+ ArtifactVersion(
+ artifact_id=artifact.id, version=1, body_markdown="Twelve requirements."
+ ),
+ )
+ return artifact
+
+
+def build_agent(memory: SqlSharedMemory, provider: ScriptedProvider) -> SampleAgent:
+ return SampleAgent(
+ memory,
+ provider,
+ ContextBuilder(memory.projects, memory.artifacts),
+ EventBus(memory.events),
+ )
+
+
+def output_payload(upstream_id: str | None, **overrides: object) -> dict[str, object]:
+ artifacts: list[dict[str, object]] = [
+ {
+ "type": ArtifactType.SYSTEM_ARCHITECTURE.value,
+ "title": "System Architecture",
+ "body_markdown": "## Components\n\nModular monolith.",
+ "content": {"components": ["api", "web"]},
+ "summary": "Initial architecture",
+ "derived_from": (
+ [
+ {
+ "upstream_artifact_id": upstream_id,
+ "kind": TraceKind.DERIVES_FROM.value,
+ "rationale": "Requirements define the domain model.",
+ }
+ ]
+ if upstream_id
+ else []
+ ),
+ }
+ ]
+ payload: dict[str, object] = {
+ "reasoning": "A modular monolith fits the stated scale.",
+ "confidence": 0.84,
+ "artifacts": artifacts,
+ "concerns": [],
+ "requires_approval": False,
+ "approval_reason": "",
+ "recommended_stack": ["FastAPI", "PostgreSQL"],
+ }
+ payload.update(overrides)
+ return payload
+
+
+# --- Execution template -------------------------------------------------------
+
+
+async def test_agent_writes_artifact_and_records_the_run(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ upstream = await add_upstream(memory, project)
+ provider = ScriptedProvider(output_payload(upstream.id))
+
+ result = await build_agent(memory, provider).run(project.id)
+
+ assert len(result.artifact_ids) == 1
+ run = await memory.runs.get(result.run_id)
+ assert run.status is AgentRunStatus.COMPLETED
+ assert run.confidence == 0.84
+ assert run.reasoning_summary.startswith("A modular monolith")
+ assert run.output_artifact_ids == result.artifact_ids
+ assert run.input_artifact_ids == [upstream.id]
+ assert run.provider == "scripted"
+
+
+async def test_agent_writes_trace_edges_at_the_consumed_version(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """The edge must cite the version actually read — ADR-0007's mechanism."""
+ upstream = await add_upstream(memory, project)
+ provider = ScriptedProvider(output_payload(upstream.id))
+
+ result = await build_agent(memory, provider).run(project.id)
+
+ edges = await memory.traces.list_for_project(project.id)
+ assert len(edges) == 1
+ assert edges[0].upstream_artifact_id == upstream.id
+ assert edges[0].downstream_artifact_id == result.artifact_ids[0]
+ assert edges[0].upstream_version == 1
+ assert edges[0].created_by_run_id == result.run_id
+
+
+async def test_downstream_goes_stale_when_upstream_is_revised(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """End to end: an agent's output falls out of date when its input changes."""
+ upstream = await add_upstream(memory, project)
+ await build_agent(memory, ScriptedProvider(output_payload(upstream.id))).run(project.id)
+
+ assert await memory.traces.stale_edges(project.id) == []
+
+ await memory.artifacts.append_version(
+ upstream.id,
+ ArtifactVersion(artifact_id=upstream.id, version=1, body_markdown="Fifteen requirements."),
+ )
+
+ stale = await memory.traces.stale_edges(project.id)
+ assert len(stale) == 1
+ assert stale[0].versions_behind == 1
+
+
+async def test_agent_publishes_lifecycle_events(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ upstream = await add_upstream(memory, project)
+
+ await build_agent(memory, ScriptedProvider(output_payload(upstream.id))).run(project.id)
+
+ types = [event.type for event in await memory.events.list_for_project(project.id)]
+ assert EventType.AGENT_STARTED in types
+ assert EventType.ARTIFACT_CREATED in types
+ assert EventType.AGENT_COMPLETED in types
+
+
+async def test_context_reaches_the_provider(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ upstream = await add_upstream(memory, project)
+ provider = ScriptedProvider(output_payload(upstream.id))
+
+ await build_agent(memory, provider).run(project.id)
+
+ sent = provider.requests[0]
+ assert "Twelve requirements." in sent.messages[0].content
+ assert "Hospital System" in sent.messages[0].content
+ assert sent.fixture_key == "software_architect.architecture"
+
+
+async def test_reviewer_feedback_is_passed_to_the_agent(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A rejection must teach, not merely repeat."""
+ upstream = await add_upstream(memory, project)
+ provider = ScriptedProvider(output_payload(upstream.id))
+
+ await build_agent(memory, provider).run(
+ project.id, feedback="Too granular — start with a modular monolith."
+ )
+
+ combined = " ".join(message.content for message in provider.requests[0].messages)
+ assert "modular monolith" in combined
+
+
+async def test_concerns_raise_a_conflict_event(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Agents flag upstream problems rather than working around them."""
+ upstream = await add_upstream(memory, project)
+ payload = output_payload(upstream.id, concerns=["Billing requirements are ambiguous."])
+
+ result = await build_agent(memory, ScriptedProvider(payload)).run(project.id)
+
+ assert result.has_concerns
+ types = [event.type for event in await memory.events.list_for_project(project.id)]
+ assert EventType.CONFLICT_DETECTED in types
+
+
+async def test_failure_marks_the_run_and_publishes(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A failed agent must be visible in the Organization view, not absent."""
+ await add_upstream(memory, project)
+
+ with pytest.raises(ProviderError):
+ await build_agent(memory, ExplodingProvider({})).run(project.id)
+
+ runs = await memory.runs.list_for_project(project.id)
+ assert runs[0].status is AgentRunStatus.FAILED
+ assert runs[0].error is not None
+ assert "upstream model unavailable" in runs[0].error
+
+ types = [event.type for event in await memory.events.list_for_project(project.id)]
+ assert EventType.AGENT_FAILED in types
+
+
+# --- Orphan guard (ADR-0007) --------------------------------------------------
+
+
+async def test_artifact_without_declared_upstream_is_rejected(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """An orphan is invisible to impact analysis, so the run fails instead."""
+ await add_upstream(memory, project)
+ provider = ScriptedProvider(output_payload(None))
+
+ with pytest.raises(ValidationError, match="declares no upstream"):
+ await build_agent(memory, provider).run(project.id)
+
+ assert await memory.artifacts.list_for_project(
+ project.id, stage=LifecycleStage.ARCHITECTURE
+ ) == []
+
+
+async def test_artifact_citing_unseen_upstream_is_rejected(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """An agent cannot claim to have used what it was never given."""
+ await add_upstream(memory, project)
+ provider = ScriptedProvider(output_payload("art_never_supplied"))
+
+ with pytest.raises(ValidationError, match="not in the agent's context"):
+ await build_agent(memory, provider).run(project.id)
+
+
+async def test_first_stage_may_produce_artifacts_with_no_upstream(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """The guard triggers on context being present, not unconditionally."""
+
+ class FirstStageAgent(SampleAgent):
+ stage = LifecycleStage.REQUIREMENT_DISCOVERY
+ role = AgentRole.PRODUCT_MANAGER
+
+ payload = output_payload(None)
+ payload["artifacts"] = [
+ {
+ "type": ArtifactType.PRD.value,
+ "title": "Product Requirements",
+ "body_markdown": "Twelve requirements.",
+ "content": {},
+ "summary": "Initial PRD",
+ "derived_from": [],
+ }
+ ]
+
+ agent = FirstStageAgent(
+ memory,
+ ScriptedProvider(payload),
+ ContextBuilder(memory.projects, memory.artifacts),
+ EventBus(memory.events),
+ )
+ result = await agent.run(project.id)
+
+ assert len(result.artifact_ids) == 1
+
+
+# --- Provider portability -----------------------------------------------------
+
+
+async def test_the_same_agent_runs_unchanged_across_providers(
+ memory: SqlSharedMemory, project: Project, tmp_path: Path
+) -> None:
+ """ADR-0004's guarantee, verified rather than asserted.
+
+ The identical agent class runs against a scripted provider and a
+ fixture-backed one with no code change — only the injected provider differs.
+ """
+ upstream = await add_upstream(memory, project)
+ payload = output_payload(upstream.id)
+
+ scripted_result = await build_agent(memory, ScriptedProvider(payload)).run(project.id)
+
+ (tmp_path / "software_architect.architecture.json").write_text(
+ json.dumps({"value": payload, "usage": {"input_tokens": 1, "output_tokens": 2}}),
+ encoding="utf-8",
+ )
+ fixture_result = await build_agent(memory, FixtureProvider(tmp_path)).run(project.id) # type: ignore[arg-type]
+
+ assert scripted_result.output.confidence == fixture_result.output.confidence
+ assert scripted_result.output.reasoning == fixture_result.output.reasoning
+ assert len(fixture_result.artifact_ids) == 1
+
+ runs = await memory.runs.list_for_project(project.id)
+ assert {run.provider for run in runs} == {"scripted", "fixture"}
+
+
+# --- Prompt loading -----------------------------------------------------------
+
+
+def test_shared_system_prompt_is_present() -> None:
+ assert "engineering_organization" in available_prompts()
+ assert "derived_from" in load_prompt("engineering_organization")
+
+
+def test_missing_prompt_lists_what_is_available() -> None:
+ with pytest.raises(PromptError) as exc_info:
+ load_prompt("no_such_agent")
+
+ assert "available" in exc_info.value.details
+
+
+def test_unsubstituted_variables_fail_loudly(tmp_path: Path) -> None:
+ """An unfilled placeholder reaching a model is far harder to diagnose."""
+ from app.agents import prompts as prompts_module
+
+ template = prompts_module.PROMPT_DIR / "_test_template.md"
+ template.write_text("Project: $project_name\nStage: $stage", encoding="utf-8")
+ prompts_module.load_prompt.cache_clear()
+
+ try:
+ assert "Hospital" in render_prompt("_test_template", project_name="Hospital", stage="x")
+
+ with pytest.raises(PromptError, match="not supplied"):
+ render_prompt("_test_template", project_name="Hospital")
+ finally:
+ template.unlink()
+ prompts_module.load_prompt.cache_clear()
diff --git a/submissions/Victorious/apps/api/tests/test_api.py b/submissions/Victorious/apps/api/tests/test_api.py
new file mode 100644
index 00000000..902810d1
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_api.py
@@ -0,0 +1,471 @@
+"""HTTP API surface.
+
+Exercised through the real app with the real container, so routing, dependency
+injection, error handling, and serialisation are all covered. The organization
+runs on the fixture provider, so no test makes a network call.
+"""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+
+import pytest
+import pytest_asyncio
+from httpx import ASGITransport, AsyncClient
+
+from app.core.config import (
+ DatabaseSettings,
+ Environment,
+ LLMProvider,
+ LLMSettings,
+ ObservabilitySettings,
+ Settings,
+)
+from app.db.session import Database
+from app.main import create_app
+
+PREFIX = "/api/v1"
+
+
+@pytest_asyncio.fixture
+async def api() -> AsyncIterator[AsyncClient]:
+ """The real application, on an isolated in-memory database."""
+ settings = Settings(
+ environment=Environment.TEST,
+ database=DatabaseSettings(
+ url="sqlite+aiosqlite:///file:apidb?mode=memory&cache=shared&uri=true"
+ ),
+ llm=LLMSettings(provider=LLMProvider.FIXTURE),
+ observability=ObservabilitySettings(log_level="ERROR", json_logs=False),
+ )
+ app = create_app(settings)
+
+ async with (
+ AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client,
+ app.router.lifespan_context(app),
+ ):
+ await app.state.container.resolve(Database).create_schema()
+ yield client
+
+
+async def create_project(api: AsyncClient, name: str = "Hospital Management System") -> str:
+ response = await api.post(
+ f"{PREFIX}/projects",
+ json={
+ "name": name,
+ "description": "Managing patients, appointments, billing, and doctors.",
+ },
+ )
+ assert response.status_code == 201, response.text
+ return response.json()["id"]
+
+
+# --- Project creation ---------------------------------------------------------
+
+
+async def test_project_is_created_from_two_fields(api: AsyncClient) -> None:
+ """07_System_Architecture.md: a name and a description, nothing else."""
+ response = await api.post(
+ f"{PREFIX}/projects",
+ json={"name": "Hospital System", "description": "Patients and appointments."},
+ )
+
+ assert response.status_code == 201
+ body = response.json()
+ assert body["id"].startswith("prj_")
+ assert body["current_stage"] == "idea"
+ assert body["completed_stages"] == 0
+ assert body["total_stages"] == 8
+
+
+async def test_empty_name_is_rejected(api: AsyncClient) -> None:
+ response = await api.post(
+ f"{PREFIX}/projects", json={"name": "", "description": "Something."}
+ )
+
+ assert response.status_code == 422
+ assert response.json()["error"]["code"] == "request_validation_error"
+
+
+async def test_unknown_project_returns_the_error_envelope(api: AsyncClient) -> None:
+ response = await api.get(f"{PREFIX}/projects/prj_missing")
+
+ assert response.status_code == 404
+ error = response.json()["error"]
+ assert error["code"] == "not_found"
+ assert error["correlation_id"]
+
+
+async def test_projects_are_listed_with_progress(api: AsyncClient) -> None:
+ await create_project(api, "First")
+ await create_project(api, "Second")
+
+ response = await api.get(f"{PREFIX}/projects")
+
+ assert response.status_code == 200
+ assert {item["name"] for item in response.json()} == {"First", "Second"}
+
+
+# --- Project detail -----------------------------------------------------------
+
+
+async def test_detail_lists_every_lifecycle_stage(api: AsyncClient) -> None:
+ """The timeline shows what happens next, not only what has happened."""
+ project_id = await create_project(api)
+
+ response = await api.get(f"{PREFIX}/projects/{project_id}")
+
+ stages = response.json()["stages"]
+ assert len(stages) == 8
+ assert stages[0]["stage"] == "requirement_discovery"
+ assert stages[0]["owner_title"] == "Product Manager"
+ assert all(stage["status"] == "pending" for stage in stages)
+
+
+# --- Organization view --------------------------------------------------------
+
+
+async def test_organization_lists_every_specialist_including_idle(
+ api: AsyncClient,
+) -> None:
+ """An agent missing from the view is indistinguishable from one that does not exist."""
+ project_id = await create_project(api)
+
+ response = await api.get(f"{PREFIX}/projects/{project_id}/agents")
+
+ cards = response.json()
+ assert len(cards) == 8
+ assert all(card["status"] == "idle" for card in cards)
+ assert {card["title"] for card in cards} >= {
+ "Product Manager",
+ "Business Analyst",
+ "Software Architect",
+ "Full Stack Engineer",
+ "QA Engineer",
+ "Documentation Engineer",
+ }
+
+
+async def test_executive_is_absent_from_the_organization_view(
+ api: AsyncClient,
+) -> None:
+ """15_Development_Guidelines.md: it coordinates, it does not perform work."""
+ project_id = await create_project(api)
+
+ cards = (await api.get(f"{PREFIX}/projects/{project_id}/agents")).json()
+
+ assert all(card["role"] != "executive" for card in cards)
+
+
+# --- Advancing the workflow ---------------------------------------------------
+
+
+async def test_advance_runs_the_organization_and_halts_at_a_gate(
+ api: AsyncClient,
+) -> None:
+ """The demo path, over HTTP, on recorded fixtures — no network involved."""
+ project_id = await create_project(api)
+
+ body = (await api.post(f"{PREFIX}/projects/{project_id}/advance")).json()
+
+ assert body["executed_stages"] == ["requirement_discovery", "business_validation"]
+ assert body["halt_action"] == "await_approval"
+ assert body["pending_approval_id"] is not None
+
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+ assert {artifact["type"] for artifact in artifacts} >= {"prd", "user_stories"}
+
+ # The gate genuinely halts: nothing downstream exists yet.
+ assert all(artifact["stage"] != "architecture" for artifact in artifacts)
+
+
+async def test_approving_a_gate_lets_the_organization_continue(
+ api: AsyncClient,
+) -> None:
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ pending = (await api.get(f"{PREFIX}/projects/{project_id}/approvals?pending=true")).json()
+ assert len(pending) == 1
+
+ decision = await api.post(
+ f"{PREFIX}/approvals/{pending[0]['id']}/decision", json={"decision": "approved"}
+ )
+ assert decision.status_code == 200
+ assert decision.json()["status"] == "approved"
+
+ body = (await api.post(f"{PREFIX}/projects/{project_id}/advance")).json()
+ assert "architecture" in body["executed_stages"]
+
+
+async def test_agents_report_their_work_after_running(api: AsyncClient) -> None:
+ """The Organization view's data, over HTTP."""
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ cards = (await api.get(f"{PREFIX}/projects/{project_id}/agents")).json()
+ by_stage = {card["stage"]: card for card in cards}
+
+ product_manager = by_stage["requirement_discovery"]
+ assert product_manager["status"] == "completed"
+ assert product_manager["confidence"] is not None
+ assert product_manager["reasoning_summary"]
+ assert product_manager["total_tokens"] > 0
+ assert by_stage["implementation"]["status"] == "idle"
+
+
+async def test_traceability_graph_connects_produced_artifacts(
+ api: AsyncClient,
+) -> None:
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ graph = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+
+ assert len(graph["nodes"]) > 0
+ assert len(graph["edges"]) > 0
+ assert graph["stale_artifact_ids"] == []
+
+
+async def test_artifact_detail_carries_body_and_history(api: AsyncClient) -> None:
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+ prd = next(item for item in artifacts if item["type"] == "prd")
+
+ detail = (
+ await api.get(f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}")
+ ).json()
+
+ assert "FR-01" in detail["body_markdown"]
+ assert detail["is_latest"] is True
+ assert detail["versions"][0]["version"] == 1
+ assert detail["content"]["functional_requirements"]
+
+
+async def test_advancing_an_unknown_project_is_a_404(api: AsyncClient) -> None:
+ response = await api.post(f"{PREFIX}/projects/prj_missing/advance")
+
+ assert response.status_code == 404
+
+
+# --- Artifacts, events, traceability ------------------------------------------
+
+
+async def test_new_project_has_no_artifacts(api: AsyncClient) -> None:
+ project_id = await create_project(api)
+
+ response = await api.get(f"{PREFIX}/projects/{project_id}/artifacts")
+
+ assert response.status_code == 200
+ assert response.json() == []
+
+
+async def test_creation_is_recorded_on_the_timeline(api: AsyncClient) -> None:
+ project_id = await create_project(api)
+
+ events = (await api.get(f"{PREFIX}/projects/{project_id}/events")).json()
+
+ assert events[0]["type"] == "project_created"
+ assert "Hospital Management System" in events[0]["summary"]
+
+
+async def test_traceability_graph_is_empty_but_well_formed(api: AsyncClient) -> None:
+ project_id = await create_project(api)
+
+ graph = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+
+ assert graph["project_id"] == project_id
+ assert graph["nodes"] == []
+ assert graph["edges"] == []
+ assert graph["stale_artifact_ids"] == []
+
+
+# --- Approvals ----------------------------------------------------------------
+
+
+async def test_pending_approvals_endpoint_is_empty_initially(api: AsyncClient) -> None:
+ await create_project(api)
+
+ response = await api.get(f"{PREFIX}/approvals")
+
+ assert response.status_code == 200
+ assert response.json() == []
+
+
+async def test_rejecting_without_feedback_is_refused(api: AsyncClient) -> None:
+ """A rejection with no reason leaves the organization to guess."""
+ response = await api.post(
+ f"{PREFIX}/approvals/apr_missing/decision",
+ json={"decision": "changes_requested"},
+ )
+
+ assert response.status_code == 422
+ assert response.json()["error"]["code"] == "validation_error"
+
+
+async def test_pending_is_not_a_decision(api: AsyncClient) -> None:
+ response = await api.post(
+ f"{PREFIX}/approvals/apr_missing/decision", json={"decision": "pending"}
+ )
+
+ assert response.status_code == 422
+
+
+async def test_deciding_an_unknown_approval_is_a_404(api: AsyncClient) -> None:
+ response = await api.post(
+ f"{PREFIX}/approvals/apr_missing/decision", json={"decision": "approved"}
+ )
+
+ assert response.status_code == 404
+
+
+# --- Contract shape -----------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+ "path",
+ [
+ "/projects",
+ "/approvals",
+ ],
+)
+async def test_collections_return_arrays(api: AsyncClient, path: str) -> None:
+ response = await api.get(f"{PREFIX}{path}")
+
+ assert response.status_code == 200
+ assert isinstance(response.json(), list)
+
+
+async def test_openapi_documents_the_workspace_surface(api: AsyncClient) -> None:
+ """The generated schema is what the web client is typed against."""
+ schema = (await api.get("/openapi.json")).json()
+ paths = schema["paths"]
+
+ assert f"{PREFIX}/projects" in paths
+ assert f"{PREFIX}/projects/{{project_id}}/advance" in paths
+ assert f"{PREFIX}/projects/{{project_id}}/traceability" in paths
+ assert f"{PREFIX}/approvals/{{approval_id}}/decision" in paths
+
+
+# --- Helix Review -------------------------------------------------------------
+
+
+async def test_every_produced_artifact_is_reviewed_as_it_lands(api: AsyncClient) -> None:
+ """Review is automatic. Nothing in the workflow asks for it."""
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+ summary = (await api.get(f"{PREFIX}/projects/{project_id}/reviews")).json()
+
+ assert summary["artifacts_reviewed"] == len(artifacts)
+ assert 0 < summary["overall_score"] <= 100
+
+
+async def test_review_summary_scores_each_specialist(api: AsyncClient) -> None:
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ summary = (await api.get(f"{PREFIX}/projects/{project_id}/reviews")).json()
+
+ assert summary["by_role"]
+ for role in summary["by_role"]:
+ assert role["artifacts_reviewed"] >= 1
+ assert 0 <= role["average_score"] <= 100
+ assert role["lowest_score"] <= role["average_score"]
+
+
+async def test_scores_are_not_uniform_across_artifacts(api: AsyncClient) -> None:
+ """A reviewer that scores everything alike has measured nothing."""
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ summary = (await api.get(f"{PREFIX}/projects/{project_id}/reviews")).json()
+ scores = {review["quality_score"] for review in summary["reviews"]}
+
+ assert len(scores) > 1
+
+
+async def test_every_review_carries_evidence_from_the_checks(api: AsyncClient) -> None:
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ summary = (await api.get(f"{PREFIX}/projects/{project_id}/reviews")).json()
+
+ for review in summary["reviews"]:
+ findings = review["strengths"] + review["weaknesses"] + review["suggestions"]
+ assert any(finding["source"] == "check" for finding in findings)
+ assert 0 <= review["deterministic_score"] <= 100
+
+
+async def test_a_review_travels_with_its_artifact(api: AsyncClient) -> None:
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+ prd = next(item for item in artifacts if item["type"] == "prd")
+
+ detail = (
+ await api.get(f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}")
+ ).json()
+
+ assert detail["review"] is not None
+ assert detail["review"]["artifact_version"] == detail["version"]
+ assert detail["review"]["artifact_id"] == prd["id"]
+
+
+async def test_a_human_revision_is_not_passed_off_as_reviewed(api: AsyncClient) -> None:
+ """Reviews are per version. A version no agent produced has none."""
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+ prd = next(item for item in artifacts if item["type"] == "prd")
+
+ await api.post(
+ f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}/revise",
+ json={"body_markdown": "# Requirements\n\nRewritten by hand.", "summary": "Human edit"},
+ )
+
+ detail = (
+ await api.get(f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}")
+ ).json()
+ assert detail["version"] == 2
+ assert detail["review"] is None
+
+ # The agent's review of v1 is still readable exactly as it was written.
+ original = (
+ await api.get(f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}?version=1")
+ ).json()
+ assert original["review"]["artifact_version"] == 1
+
+
+async def test_reviews_for_a_project_with_no_work_are_empty_but_well_formed(
+ api: AsyncClient,
+) -> None:
+ project_id = await create_project(api)
+
+ summary = (await api.get(f"{PREFIX}/projects/{project_id}/reviews")).json()
+
+ assert summary["artifacts_reviewed"] == 0
+ assert summary["overall_score"] == 0
+ assert summary["by_role"] == []
+ assert summary["reviews"] == []
+
+
+async def test_recommendations_are_specific_and_never_repeat(api: AsyncClient) -> None:
+ """A template sentence three times reads as noise, not emphasis."""
+ project_id = await create_project(api)
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ summary = (await api.get(f"{PREFIX}/projects/{project_id}/reviews")).json()
+ texts = [item["text"] for item in summary["recommendations"]]
+
+ assert texts
+ assert len(texts) == len(set(texts))
+ # Specific before generic: the model's suggestions name something in the
+ # artifact, the checks' suggestions are templates.
+ sources = [item["source"] for item in summary["recommendations"]]
+ assert sources == sorted(sources, key=lambda source: source != "reasoning")
diff --git a/submissions/Victorious/apps/api/tests/test_approvals.py b/submissions/Victorious/apps/api/tests/test_approvals.py
new file mode 100644
index 00000000..2ca98b6f
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_approvals.py
@@ -0,0 +1,414 @@
+"""The approval loop: gates, rejection, revision, and re-approval.
+
+`09_MVP_Roadmap.md` requires human approval of requirements, architecture,
+technology selection, major engineering decisions, and final code generation.
+Three are stage gates; two are raised by agents. Both paths are exercised here,
+along with what happens when a reviewer says no.
+"""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+
+import pytest_asyncio
+from httpx import ASGITransport, AsyncClient
+
+from app.core.config import (
+ DatabaseSettings,
+ Environment,
+ LLMProvider,
+ LLMSettings,
+ ObservabilitySettings,
+ Settings,
+)
+from app.db.session import Database
+from app.domain.approvals import ApprovalKind
+from app.domain.artifacts import ArtifactStatus, ArtifactType
+from app.domain.lifecycle import LifecycleStage, StageStatus
+from app.main import create_app
+from app.memory.repository import SharedMemory
+
+PREFIX = "/api/v1"
+
+
+@pytest_asyncio.fixture
+async def api() -> AsyncIterator[AsyncClient]:
+ settings = Settings(
+ environment=Environment.TEST,
+ database=DatabaseSettings(
+ url="sqlite+aiosqlite:///file:approvaldb?mode=memory&cache=shared&uri=true"
+ ),
+ llm=LLMSettings(provider=LLMProvider.FIXTURE),
+ observability=ObservabilitySettings(log_level="ERROR", json_logs=False),
+ )
+ app = create_app(settings)
+
+ async with (
+ AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client,
+ app.router.lifespan_context(app),
+ ):
+ await app.state.container.resolve(Database).create_schema()
+ client.app_ref = app # type: ignore[attr-defined]
+ yield client
+
+
+def memory_of(api: AsyncClient) -> SharedMemory:
+ return api.app_ref.state.container.resolve(SharedMemory) # type: ignore[attr-defined,type-abstract]
+
+
+async def start_project(api: AsyncClient) -> str:
+ response = await api.post(
+ f"{PREFIX}/projects",
+ json={
+ "name": "Hospital Management System",
+ "description": "Patients, appointments, billing, doctors, and operations.",
+ },
+ )
+ project_id: str = response.json()["id"]
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+ return project_id
+
+
+async def pending(api: AsyncClient, project_id: str) -> list[dict]:
+ response = await api.get(f"{PREFIX}/projects/{project_id}/approvals?pending=true")
+ return list(response.json())
+
+
+async def decide(
+ api: AsyncClient, approval_id: str, decision: str, feedback: str | None = None
+) -> dict:
+ response = await api.post(
+ f"{PREFIX}/approvals/{approval_id}/decision",
+ json={"decision": decision, "feedback": feedback},
+ )
+ assert response.status_code == 200, response.text
+ return dict(response.json())
+
+
+# --- The gate halts -----------------------------------------------------------
+
+
+async def test_gate_blocks_downstream_work(api: AsyncClient) -> None:
+ """12_Risk_Analysis.md: a gate that notified while work continued is not a gate."""
+ project_id = await start_project(api)
+
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+
+ assert [item for item in artifacts if item["stage"] == "requirement_discovery"]
+ assert not [item for item in artifacts if item["stage"] == "architecture"]
+
+
+async def test_approving_marks_the_reviewed_artifacts_approved(
+ api: AsyncClient,
+) -> None:
+ """A sign-off must be visible everywhere, not only on the approval record."""
+ project_id = await start_project(api)
+ request = (await pending(api, project_id))[0]
+
+ await decide(api, request["id"], "approved")
+
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+ reviewed = {item["id"] for item in request["artifacts"]}
+ approved = {item["id"] for item in artifacts if item["status"] == "approved"}
+
+ assert reviewed
+ assert reviewed <= approved
+
+
+# --- Rejection ----------------------------------------------------------------
+
+
+async def test_rejection_reopens_the_stage_that_produced_the_work(
+ api: AsyncClient,
+) -> None:
+ """The problem is with the work, so its author runs again — not the blocked stage."""
+ project_id = await start_project(api)
+ request = (await pending(api, project_id))[0]
+
+ await decide(
+ api, request["id"], "changes_requested", "Billing scope is unclear — split it out."
+ )
+
+ project = (await api.get(f"{PREFIX}/projects/{project_id}")).json()
+ by_stage = {stage["stage"]: stage["status"] for stage in project["stages"]}
+
+ assert by_stage["requirement_discovery"] == "pending"
+
+
+async def test_rejection_feedback_reaches_the_agent_on_rerun(
+ api: AsyncClient,
+) -> None:
+ """A rejection must teach. The feedback travels into the agent's context."""
+ project_id = await start_project(api)
+ request = (await pending(api, project_id))[0]
+
+ await decide(api, request["id"], "changes_requested", "Split billing out.")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ memory = memory_of(api)
+ runs = await memory.runs.list_for_project(project_id)
+ discovery_runs = [
+ run for run in runs if run.stage is LifecycleStage.REQUIREMENT_DISCOVERY
+ ]
+
+ assert len(discovery_runs) == 2, "the Product Manager must have run again"
+
+
+async def test_rerun_revises_rather_than_duplicating(api: AsyncClient) -> None:
+ """A second run produces v2 of the same artifact, not a competing copy.
+
+ Duplicating would fork the traceability graph and trigger the duplicate
+ authority conflict; versioning keeps one stable identity across revisions.
+ """
+ project_id = await start_project(api)
+ request = (await pending(api, project_id))[0]
+
+ before = (
+ await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")
+ ).json()
+ assert len(before) == 1
+ assert before[0]["current_version"] == 1
+
+ await decide(api, request["id"], "changes_requested", "Split billing out.")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ after = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()
+
+ assert len(after) == 1, "revising must not create a second PRD"
+ assert after[0]["id"] == before[0]["id"], "identity must survive the revision"
+ assert after[0]["current_version"] == 2
+
+
+async def test_revised_work_is_no_longer_approved(api: AsyncClient) -> None:
+ """Answering a rejection with content nobody has reviewed must not stay approved."""
+ project_id = await start_project(api)
+ first = (await pending(api, project_id))[0]
+ await decide(api, first["id"], "approved")
+
+ memory = memory_of(api)
+ prd = (
+ await memory.artifacts.list_for_project(project_id, artifact_type=ArtifactType.PRD)
+ )[0]
+ assert prd.status is ArtifactStatus.APPROVED
+
+ # Reopen the stage by rejecting a later gate covering the same artifacts.
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+ architecture_gate = (await pending(api, project_id))[0]
+ await decide(api, architecture_gate["id"], "changes_requested", "Reconsider the split.")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ revised = await memory.artifacts.get(
+ (
+ await memory.artifacts.list_for_project(
+ project_id, artifact_type=ArtifactType.SYSTEM_ARCHITECTURE
+ )
+ )[0].id
+ )
+ assert revised.status is ArtifactStatus.DRAFT
+
+
+async def test_a_fresh_gate_is_raised_after_revision(api: AsyncClient) -> None:
+ """A rejection applies to the version reviewed, not to the project forever.
+
+ Without this the project would deadlock: the old decision would keep blocking
+ a stage whose inputs have since been rewritten.
+ """
+ project_id = await start_project(api)
+ first = (await pending(api, project_id))[0]
+
+ await decide(api, first["id"], "changes_requested", "Split billing out.")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ outstanding = await pending(api, project_id)
+
+ assert outstanding, "the revised work must come back for a decision"
+ assert outstanding[0]["id"] != first["id"]
+ assert outstanding[0]["kind"] == ApprovalKind.REQUIREMENTS.value
+
+
+async def test_the_project_recovers_after_a_rejection(api: AsyncClient) -> None:
+ """End to end: reject, revise, approve, and the lifecycle continues."""
+ project_id = await start_project(api)
+
+ first = (await pending(api, project_id))[0]
+ await decide(api, first["id"], "changes_requested", "Split billing out.")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ second = (await pending(api, project_id))[0]
+ await decide(api, second["id"], "approved")
+ result = (await api.post(f"{PREFIX}/projects/{project_id}/advance")).json()
+
+ assert "architecture" in result["executed_stages"]
+
+
+# --- Agent-requested gates ----------------------------------------------------
+
+
+async def test_agent_can_raise_its_own_approval_gate(api: AsyncClient) -> None:
+ """09_MVP_Roadmap.md requires technology selection to be approved.
+
+ That gate is not stage-shaped — it exists because the architect concluded its
+ choice was expensive to reverse, so the agent raises it.
+ """
+ project_id = await start_project(api)
+ await decide(api, (await pending(api, project_id))[0]["id"], "approved")
+
+ # Architecture runs, and its agent sets requires_approval.
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ memory = memory_of(api)
+ architect_run = next(
+ run
+ for run in await memory.runs.list_for_project(project_id)
+ if run.stage is LifecycleStage.ARCHITECTURE
+ )
+ assert architect_run.requires_approval is True
+ assert architect_run.approval_reason
+
+ kinds = {item["kind"] for item in await pending(api, project_id)}
+ assert ApprovalKind.TECHNOLOGY_SELECTION.value in kinds
+
+
+async def test_agent_gate_reviews_what_the_agent_produced(api: AsyncClient) -> None:
+ """A stage gate protects inputs; an agent gate reviews outputs."""
+ project_id = await start_project(api)
+ await decide(api, (await pending(api, project_id))[0]["id"], "approved")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ gate = next(
+ item
+ for item in await pending(api, project_id)
+ if item["kind"] == ApprovalKind.TECHNOLOGY_SELECTION.value
+ )
+ types = {artifact["type"] for artifact in gate["artifacts"]}
+
+ assert ArtifactType.TECHNOLOGY_DECISION.value in types
+
+
+async def test_agent_gate_is_not_raised_twice(api: AsyncClient) -> None:
+ project_id = await start_project(api)
+ await decide(api, (await pending(api, project_id))[0]["id"], "approved")
+
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ technology_gates = [
+ item
+ for item in (await api.get(f"{PREFIX}/projects/{project_id}/approvals")).json()
+ if item["kind"] == ApprovalKind.TECHNOLOGY_SELECTION.value
+ ]
+
+ assert len(technology_gates) == 1
+
+
+# --- Human revision -----------------------------------------------------------
+
+
+async def test_revising_an_artifact_appends_a_version(api: AsyncClient) -> None:
+ """The version downstream agents consumed must stay readable."""
+ project_id = await start_project(api)
+ prd = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()[0]
+
+ response = await api.post(
+ f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}/revise",
+ json={
+ "body_markdown": "# Requirements (revised)\n\nBilling is now its own requirement.",
+ "summary": "Split billing out",
+ },
+ )
+
+ assert response.status_code == 200
+ detail = response.json()
+ assert detail["version"] == 2
+ assert len(detail["versions"]) == 2
+ assert "Billing is now its own requirement" in detail["body_markdown"]
+
+ original = (
+ await api.get(
+ f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}?version=1"
+ )
+ ).json()
+ assert "FR-01" in original["body_markdown"]
+
+
+async def test_revising_makes_downstream_work_stale(api: AsyncClient) -> None:
+ """The differentiator, through the API a user actually calls."""
+ project_id = await start_project(api)
+ await decide(api, (await pending(api, project_id))[0]["id"], "approved")
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ graph_before = (
+ await api.get(f"{PREFIX}/projects/{project_id}/traceability")
+ ).json()
+ assert graph_before["stale_artifact_ids"] == []
+
+ prd = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()[0]
+ await api.post(
+ f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}/revise",
+ json={"body_markdown": "# Requirements (revised)", "summary": "Scope change"},
+ )
+
+ graph_after = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+
+ assert graph_after["stale_artifact_ids"], "downstream work must be flagged stale"
+ assert any(edge["is_stale"] for edge in graph_after["edges"])
+
+
+async def test_revising_an_artifact_from_another_project_is_refused(
+ api: AsyncClient,
+) -> None:
+ project_id = await start_project(api)
+ other = (
+ await api.post(
+ f"{PREFIX}/projects", json={"name": "Other", "description": "Unrelated."}
+ )
+ ).json()["id"]
+
+ prd = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()[0]
+
+ response = await api.post(
+ f"{PREFIX}/projects/{other}/artifacts/{prd['id']}/revise",
+ json={"body_markdown": "Tampered."},
+ )
+
+ assert response.status_code == 422
+ assert response.json()["error"]["code"] == "validation_error"
+
+
+# --- Timeline -----------------------------------------------------------------
+
+
+async def test_decisions_appear_on_the_timeline(api: AsyncClient) -> None:
+ project_id = await start_project(api)
+ request = (await pending(api, project_id))[0]
+
+ await decide(api, request["id"], "approved")
+
+ events = (await api.get(f"{PREFIX}/projects/{project_id}/events")).json()
+ types = {event["type"] for event in events}
+
+ assert "approval_requested" in types
+ assert "approval_granted" in types
+
+
+async def test_rejection_appears_on_the_timeline(api: AsyncClient) -> None:
+ project_id = await start_project(api)
+ request = (await pending(api, project_id))[0]
+
+ await decide(api, request["id"], "changes_requested", "Not specific enough.")
+
+ events = (await api.get(f"{PREFIX}/projects/{project_id}/events")).json()
+
+ assert any(event["type"] == "approval_rejected" for event in events)
+ assert any("Changes Requested" in event["summary"] for event in events)
+
+
+async def test_stage_status_reflects_the_pending_gate(api: AsyncClient) -> None:
+ project_id = await start_project(api)
+
+ project = (await api.get(f"{PREFIX}/projects/{project_id}")).json()
+ architecture = next(
+ stage for stage in project["stages"] if stage["stage"] == "architecture"
+ )
+
+ assert architecture["status"] == StageStatus.AWAITING_APPROVAL.value
diff --git a/submissions/Victorious/apps/api/tests/test_architecture.py b/submissions/Victorious/apps/api/tests/test_architecture.py
new file mode 100644
index 00000000..3dca7754
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_architecture.py
@@ -0,0 +1,135 @@
+"""Executable architecture rules.
+
+`15_Development_Guidelines.md` requires clean architecture and warns that
+architectural quality must never be sacrificed for implementation speed. A
+document cannot enforce that; a test can.
+
+These tests parse the source tree and fail the build when a layering rule is
+violated, so the boundary holds under time pressure instead of eroding quietly.
+"""
+
+from __future__ import annotations
+
+import ast
+from pathlib import Path
+
+import pytest
+
+APP_ROOT = Path(__file__).resolve().parent.parent / "app"
+
+# Dependencies point inward. Each layer may import only from itself, the layers
+# beneath it, and `core` (cross-cutting infrastructure). `domain` sits innermost
+# and may import nothing internal at all.
+_FORBIDDEN_IMPORTS: dict[str, tuple[str, ...]] = {
+ "domain": ("app.core", "app.api", "app.db", "app.llm", "app.memory",
+ "app.agents", "app.orchestration", "app.events"),
+ "db": ("app.api", "app.memory", "app.llm", "app.agents", "app.orchestration",
+ "app.events"),
+ "memory": ("app.api", "app.agents", "app.orchestration"),
+ # The review layer is a sibling of agents, not a consumer of them, and must
+ # never reach for Mutagent: 07_System_Architecture.md places it outside the
+ # runtime execution path.
+ "review": ("app.api", "app.agents", "app.orchestration"),
+ "events": ("app.api", "app.agents", "app.orchestration", "app.llm"),
+ "llm": ("app.api", "app.agents", "app.orchestration", "app.memory"),
+ "agents": ("app.api", "app.orchestration"),
+ "orchestration": ("app.api",),
+}
+
+# `domain` must also stay free of frameworks so it can run without I/O.
+_FORBIDDEN_THIRD_PARTY_IN_DOMAIN = (
+ "fastapi", "starlette", "sqlalchemy", "anthropic", "google",
+ "langgraph", "chromadb", "redis", "httpx", "uvicorn",
+)
+
+
+def _iter_modules(layer: str) -> list[Path]:
+ """Return every Python module in a layer, or an empty list if absent.
+
+ Layers arrive in later milestones; a rule for a layer that does not exist yet
+ is simply inert rather than an error.
+ """
+ layer_path = APP_ROOT / layer
+ if not layer_path.is_dir():
+ return []
+ return sorted(layer_path.rglob("*.py"))
+
+
+def _imported_roots(module_path: Path) -> set[str]:
+ """Return every module path imported by ``module_path``.
+
+ Uses the AST rather than importing, so the check is static and cannot be
+ defeated by import-time side effects.
+ """
+ tree = ast.parse(module_path.read_text(encoding="utf-8"), filename=str(module_path))
+ imported: set[str] = set()
+
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Import):
+ imported.update(alias.name for alias in node.names)
+ elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
+ imported.add(node.module)
+
+ return imported
+
+
+@pytest.mark.parametrize("layer", sorted(_FORBIDDEN_IMPORTS))
+def test_layer_does_not_import_outward(layer: str) -> None:
+ """No layer may depend on a layer above it."""
+ forbidden = _FORBIDDEN_IMPORTS[layer]
+ violations: list[str] = []
+
+ for module_path in _iter_modules(layer):
+ for imported in _imported_roots(module_path):
+ for banned in forbidden:
+ if imported == banned or imported.startswith(f"{banned}."):
+ relative = module_path.relative_to(APP_ROOT.parent)
+ violations.append(f"{relative} imports {imported}")
+
+ assert not violations, (
+ f"Layer '{layer}' violates inward-dependency rule:\n " + "\n ".join(violations)
+ )
+
+
+def test_domain_is_framework_free() -> None:
+ """The domain layer must not depend on any framework or client library."""
+ violations: list[str] = []
+
+ for module_path in _iter_modules("domain"):
+ for imported in _imported_roots(module_path):
+ root = imported.split(".")[0]
+ if root in _FORBIDDEN_THIRD_PARTY_IN_DOMAIN:
+ relative = module_path.relative_to(APP_ROOT.parent)
+ violations.append(f"{relative} imports {imported}")
+
+ assert not violations, (
+ "Domain layer must stay framework-free:\n " + "\n ".join(violations)
+ )
+
+
+def test_domain_layer_exists_and_is_populated() -> None:
+ """Guard against the rules above passing vacuously on an empty domain."""
+ modules = _iter_modules("domain")
+ assert modules, "app/domain must exist — it is the innermost architectural layer"
+ assert any(m.name != "__init__.py" for m in modules), "app/domain contains no modules"
+
+
+def test_composition_root_is_the_only_wiring_point() -> None:
+ """Only ``bootstrap`` may construct the container.
+
+ Keeps implementation choices in one auditable file rather than scattered
+ across the codebase.
+ """
+ violations: list[str] = []
+
+ for module_path in APP_ROOT.rglob("*.py"):
+ if module_path.name in {"bootstrap.py", "container.py"}:
+ continue
+ source = module_path.read_text(encoding="utf-8")
+ if "Container()" in source:
+ violations.append(str(module_path.relative_to(APP_ROOT.parent)))
+
+ assert not violations, (
+ "Container must only be constructed in app/core/bootstrap.py, found in:\n "
+ + "\n ".join(violations)
+ )
diff --git a/submissions/Victorious/apps/api/tests/test_config.py b/submissions/Victorious/apps/api/tests/test_config.py
new file mode 100644
index 00000000..feb62abc
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_config.py
@@ -0,0 +1,57 @@
+"""Configuration parsing and environment overrides."""
+
+from __future__ import annotations
+
+import pytest
+
+from app.core.config import Environment, LLMProvider, Settings, get_settings
+
+
+def test_defaults_are_safe_for_local_development() -> None:
+ settings = Settings()
+
+ assert settings.environment is Environment.LOCAL
+ assert settings.llm.provider is LLMProvider.ANTHROPIC
+ assert settings.database.url.startswith("sqlite")
+ assert settings.vector_store.enabled is False
+
+
+def test_nested_settings_come_from_delimited_env_vars(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setenv("VICTORIOUS_LLM__PROVIDER", "gemini")
+ monkeypatch.setenv("VICTORIOUS_DATABASE__ECHO", "true")
+
+ settings = Settings()
+
+ assert settings.llm.provider is LLMProvider.GEMINI
+ assert settings.database.echo is True
+
+
+def test_cors_origins_accept_a_comma_separated_string(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ """A single env var must be able to carry a list — compose passes strings."""
+ monkeypatch.setenv(
+ "VICTORIOUS_CORS_ORIGINS", "http://localhost:3000, https://victorious.app"
+ )
+
+ assert Settings().cors_origins == ["http://localhost:3000", "https://victorious.app"]
+
+
+def test_docs_are_disabled_in_production() -> None:
+ assert Settings(environment=Environment.PRODUCTION).docs_url is None
+ assert Settings(environment=Environment.LOCAL).docs_url == "/docs"
+
+
+def test_api_keys_are_hidden_from_repr() -> None:
+ """Secrets must not reach logs through an accidental repr."""
+ settings = Settings()
+ settings.llm.anthropic_api_key = "sk-should-not-appear"
+
+ assert "sk-should-not-appear" not in repr(settings.llm)
+
+
+def test_get_settings_is_cached() -> None:
+ get_settings.cache_clear()
+ assert get_settings() is get_settings()
diff --git a/submissions/Victorious/apps/api/tests/test_conflicts.py b/submissions/Victorious/apps/api/tests/test_conflicts.py
new file mode 100644
index 00000000..713b0d85
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_conflicts.py
@@ -0,0 +1,231 @@
+"""Conflict detection — pure, no database."""
+
+from __future__ import annotations
+
+from app.domain.agents import AgentRun, AgentRunStatus
+from app.domain.artifacts import Artifact, ArtifactStatus, ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.traceability import TraceEdge
+from app.orchestration.conflicts import (
+ ConflictKind,
+ ConflictSeverity,
+ blocking,
+ detect_conflicts,
+)
+
+PROJECT = "prj_test"
+
+
+def artifact(
+ artifact_type: ArtifactType,
+ *,
+ title: str | None = None,
+ status: ArtifactStatus = ArtifactStatus.DRAFT,
+ stage: LifecycleStage = LifecycleStage.ARCHITECTURE,
+ role: AgentRole = AgentRole.SOFTWARE_ARCHITECT,
+) -> Artifact:
+ return Artifact(
+ project_id=PROJECT,
+ type=artifact_type,
+ title=title or artifact_type.value,
+ stage=stage,
+ owner_role=role,
+ status=status,
+ current_version=1,
+ )
+
+
+def run(
+ *,
+ role: AgentRole = AgentRole.SOFTWARE_ARCHITECT,
+ confidence: float | None = 0.9,
+ outputs: list[str] | None = None,
+) -> AgentRun:
+ return AgentRun(
+ project_id=PROJECT,
+ role=role,
+ stage=LifecycleStage.ARCHITECTURE,
+ status=AgentRunStatus.COMPLETED,
+ confidence=confidence,
+ output_artifact_ids=outputs or [],
+ )
+
+
+def test_clean_project_has_no_conflicts() -> None:
+ assert (
+ detect_conflicts(artifacts=[], edges=[], current_versions={}, runs=[]) == []
+ )
+
+
+# --- Stale derivation ---------------------------------------------------------
+
+
+def test_stale_derivation_is_blocking() -> None:
+ """Continuing on a stale derivation compounds the inconsistency."""
+ prd = artifact(ArtifactType.PRD, title="Requirements")
+ architecture = artifact(ArtifactType.SYSTEM_ARCHITECTURE, title="Architecture")
+
+ conflicts = detect_conflicts(
+ artifacts=[prd, architecture],
+ edges=[
+ TraceEdge(
+ project_id=PROJECT,
+ upstream_artifact_id=prd.id,
+ downstream_artifact_id=architecture.id,
+ upstream_version=1,
+ )
+ ],
+ current_versions={prd.id: 3, architecture.id: 1},
+ runs=[],
+ )
+
+ assert len(conflicts) == 1
+ assert conflicts[0].kind is ConflictKind.STALE_DERIVATION
+ assert conflicts[0].severity is ConflictSeverity.BLOCKING
+ assert conflicts[0].detail["versions_behind"] == 2
+ assert "Architecture" in conflicts[0].summary
+ assert "Requirements" in conflicts[0].summary
+
+
+def test_current_derivation_is_not_flagged() -> None:
+ prd = artifact(ArtifactType.PRD)
+ architecture = artifact(ArtifactType.SYSTEM_ARCHITECTURE)
+
+ conflicts = detect_conflicts(
+ artifacts=[prd, architecture],
+ edges=[
+ TraceEdge(
+ project_id=PROJECT,
+ upstream_artifact_id=prd.id,
+ downstream_artifact_id=architecture.id,
+ upstream_version=2,
+ )
+ ],
+ current_versions={prd.id: 2},
+ runs=[],
+ )
+
+ assert conflicts == []
+
+
+# --- Duplicate authority ------------------------------------------------------
+
+
+def test_two_approved_artifacts_of_one_type_is_blocking() -> None:
+ """15_Development_Guidelines.md: shared memory is *the* single source of truth."""
+ first = artifact(ArtifactType.SYSTEM_ARCHITECTURE, status=ArtifactStatus.APPROVED)
+ second = artifact(ArtifactType.SYSTEM_ARCHITECTURE, status=ArtifactStatus.APPROVED)
+
+ conflicts = detect_conflicts(
+ artifacts=[first, second], edges=[], current_versions={}, runs=[]
+ )
+
+ assert len(conflicts) == 1
+ assert conflicts[0].kind is ConflictKind.DUPLICATE_AUTHORITY
+ assert conflicts[0].severity is ConflictSeverity.BLOCKING
+ assert set(conflicts[0].artifact_ids) == {first.id, second.id}
+
+
+def test_competing_drafts_are_allowed() -> None:
+ """Only approved artifacts claim authority; drafts are work in progress."""
+ conflicts = detect_conflicts(
+ artifacts=[
+ artifact(ArtifactType.SYSTEM_ARCHITECTURE),
+ artifact(ArtifactType.SYSTEM_ARCHITECTURE),
+ ],
+ edges=[],
+ current_versions={},
+ runs=[],
+ )
+
+ assert conflicts == []
+
+
+def test_different_types_do_not_collide() -> None:
+ conflicts = detect_conflicts(
+ artifacts=[
+ artifact(ArtifactType.SYSTEM_ARCHITECTURE, status=ArtifactStatus.APPROVED),
+ artifact(ArtifactType.API_CONTRACT, status=ArtifactStatus.APPROVED),
+ ],
+ edges=[],
+ current_versions={},
+ runs=[],
+ )
+
+ assert conflicts == []
+
+
+# --- Concerns and confidence --------------------------------------------------
+
+
+def test_agent_concerns_are_advisory() -> None:
+ """Escalating every concern to blocking would deter agents from raising them."""
+ architect = run()
+
+ conflicts = detect_conflicts(
+ artifacts=[],
+ edges=[],
+ current_versions={},
+ runs=[architect],
+ concerns_by_run={architect.id: ["Billing requirements are ambiguous."]},
+ )
+
+ assert len(conflicts) == 1
+ assert conflicts[0].kind is ConflictKind.UNRESOLVED_CONCERN
+ assert conflicts[0].severity is ConflictSeverity.ADVISORY
+ assert "Billing requirements are ambiguous." in conflicts[0].summary
+
+
+def test_low_confidence_is_advisory() -> None:
+ """Blocking on low confidence would pressure agents toward inflated scores."""
+ conflicts = detect_conflicts(
+ artifacts=[], edges=[], current_versions={}, runs=[run(confidence=0.3)]
+ )
+
+ assert len(conflicts) == 1
+ assert conflicts[0].kind is ConflictKind.LOW_CONFIDENCE
+ assert conflicts[0].severity is ConflictSeverity.ADVISORY
+ assert "30%" in conflicts[0].summary
+
+
+def test_confident_runs_are_not_flagged() -> None:
+ conflicts = detect_conflicts(
+ artifacts=[], edges=[], current_versions={}, runs=[run(confidence=0.85)]
+ )
+
+ assert conflicts == []
+
+
+def test_runs_without_confidence_are_not_flagged() -> None:
+ """A run still in flight has no score yet; absence is not low confidence."""
+ conflicts = detect_conflicts(
+ artifacts=[], edges=[], current_versions={}, runs=[run(confidence=None)]
+ )
+
+ assert conflicts == []
+
+
+# --- Aggregation --------------------------------------------------------------
+
+
+def test_blocking_conflicts_are_ordered_first() -> None:
+ prd = artifact(ArtifactType.PRD)
+ architecture = artifact(ArtifactType.SYSTEM_ARCHITECTURE)
+
+ conflicts = detect_conflicts(
+ artifacts=[prd, architecture],
+ edges=[
+ TraceEdge(
+ project_id=PROJECT,
+ upstream_artifact_id=prd.id,
+ downstream_artifact_id=architecture.id,
+ upstream_version=1,
+ )
+ ],
+ current_versions={prd.id: 2},
+ runs=[run(confidence=0.2)],
+ )
+
+ assert len(conflicts) == 2
+ assert conflicts[0].severity is ConflictSeverity.BLOCKING
+ assert len(blocking(conflicts)) == 1
diff --git a/submissions/Victorious/apps/api/tests/test_container.py b/submissions/Victorious/apps/api/tests/test_container.py
new file mode 100644
index 00000000..ed449d46
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_container.py
@@ -0,0 +1,122 @@
+"""Dependency injection container behaviour."""
+
+from __future__ import annotations
+
+from typing import Protocol
+
+import pytest
+
+from app.core.container import Container, ContainerError
+
+
+class Greeter(Protocol):
+ def greet(self) -> str: ...
+
+
+class EnglishGreeter:
+ def greet(self) -> str:
+ return "hello"
+
+
+class FrenchGreeter:
+ def greet(self) -> str:
+ return "bonjour"
+
+
+def test_resolves_registered_singleton() -> None:
+ container = Container()
+ container.register_singleton(Greeter, EnglishGreeter) # type: ignore[type-abstract]
+
+ assert container.resolve(Greeter).greet() == "hello" # type: ignore[type-abstract]
+
+
+def test_singleton_returns_the_same_instance() -> None:
+ container = Container()
+ container.register_singleton(Greeter, EnglishGreeter) # type: ignore[type-abstract]
+
+ assert container.resolve(Greeter) is container.resolve(Greeter) # type: ignore[type-abstract]
+
+
+def test_factory_returns_a_new_instance_each_time() -> None:
+ container = Container()
+ container.register_factory(Greeter, EnglishGreeter) # type: ignore[type-abstract]
+
+ assert container.resolve(Greeter) is not container.resolve(Greeter) # type: ignore[type-abstract]
+
+
+def test_singleton_factory_is_lazy() -> None:
+ """Nothing is constructed until first resolution."""
+ constructed = False
+
+ def build() -> EnglishGreeter:
+ nonlocal constructed
+ constructed = True
+ return EnglishGreeter()
+
+ container = Container()
+ container.register_singleton(Greeter, build) # type: ignore[type-abstract]
+
+ assert constructed is False
+ container.resolve(Greeter) # type: ignore[type-abstract]
+ assert constructed is True
+
+
+def test_implementation_can_be_swapped_without_touching_call_sites() -> None:
+ """The property the whole architecture depends on.
+
+ Swapping Anthropic for Gemini, or SQL memory for any other store, is exactly
+ this operation performed in the composition root.
+ """
+ container = Container()
+ container.register_singleton(Greeter, EnglishGreeter) # type: ignore[type-abstract]
+ assert container.resolve(Greeter).greet() == "hello" # type: ignore[type-abstract]
+
+ container.clear()
+ container.register_singleton(Greeter, FrenchGreeter) # type: ignore[type-abstract]
+ assert container.resolve(Greeter).greet() == "bonjour" # type: ignore[type-abstract]
+
+
+def test_resolving_unregistered_protocol_fails_loudly() -> None:
+ container = Container()
+
+ with pytest.raises(ContainerError, match="No implementation registered"):
+ container.resolve(Greeter) # type: ignore[type-abstract]
+
+
+async def test_aclose_disposes_singletons() -> None:
+ closed = False
+
+ class Closable:
+ async def aclose(self) -> None:
+ nonlocal closed
+ closed = True
+
+ container = Container()
+ container.register_instance(Closable, Closable())
+
+ await container.aclose()
+
+ assert closed is True
+ assert container.has(Closable) is False
+
+
+async def test_aclose_survives_a_failing_disposer() -> None:
+ """Shutdown must be best-effort: one bad disposer cannot block the rest."""
+ second_closed = False
+
+ class Exploding:
+ def close(self) -> None:
+ raise RuntimeError("cannot close")
+
+ class Fine:
+ def close(self) -> None:
+ nonlocal second_closed
+ second_closed = True
+
+ container = Container()
+ container.register_instance(Exploding, Exploding())
+ container.register_instance(Fine, Fine())
+
+ await container.aclose()
+
+ assert second_closed is True
diff --git a/submissions/Victorious/apps/api/tests/test_context_builder.py b/submissions/Victorious/apps/api/tests/test_context_builder.py
new file mode 100644
index 00000000..441637c3
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_context_builder.py
@@ -0,0 +1,286 @@
+"""Context assembly: scoping, prioritisation, and budgeting."""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+
+import pytest_asyncio
+
+from app.core.config import DatabaseSettings
+from app.db.session import Database
+from app.domain.artifacts import Artifact, ArtifactStatus, ArtifactType, ArtifactVersion
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.projects import Project
+from app.memory.context_builder import CHARS_PER_TOKEN, ContextBuilder
+from app.memory.sql_repository import SqlSharedMemory
+
+
+@pytest_asyncio.fixture
+async def memory() -> AsyncIterator[SqlSharedMemory]:
+ database = Database(
+ DatabaseSettings(url="sqlite+aiosqlite:///file:ctxdb?mode=memory&cache=shared&uri=true")
+ )
+ await database.create_schema()
+ try:
+ yield SqlSharedMemory(database)
+ finally:
+ await database.aclose()
+
+
+@pytest_asyncio.fixture
+async def project(memory: SqlSharedMemory) -> Project:
+ return await memory.projects.create(
+ Project(name="Hospital System", description="Patients, appointments, billing.")
+ )
+
+
+async def add_artifact(
+ memory: SqlSharedMemory,
+ project: Project,
+ *,
+ title: str,
+ stage: LifecycleStage,
+ body: str,
+ artifact_type: ArtifactType = ArtifactType.PRD,
+ role: AgentRole = AgentRole.PRODUCT_MANAGER,
+ status: ArtifactStatus = ArtifactStatus.DRAFT,
+) -> Artifact:
+ artifact = await memory.artifacts.create(
+ Artifact(
+ project_id=project.id,
+ type=artifact_type,
+ title=title,
+ stage=stage,
+ owner_role=role,
+ status=status,
+ )
+ )
+ await memory.artifacts.append_version(
+ artifact.id,
+ ArtifactVersion(artifact_id=artifact.id, version=1, body_markdown=body),
+ )
+ return artifact
+
+
+def builder(memory: SqlSharedMemory, *, token_budget: int = 24_000) -> ContextBuilder:
+ return ContextBuilder(memory.projects, memory.artifacts, token_budget=token_budget)
+
+
+async def test_first_stage_has_no_upstream_context(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ context = await builder(memory).build(
+ project.id,
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ role=AgentRole.PRODUCT_MANAGER,
+ )
+
+ assert context.entries == []
+ assert "first stage" in context.render()
+
+
+async def test_upstream_artifacts_are_included(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ await add_artifact(
+ memory,
+ project,
+ title="Product Requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="Twelve functional requirements.",
+ )
+
+ context = await builder(memory).build(
+ project.id, stage=LifecycleStage.ARCHITECTURE, role=AgentRole.SOFTWARE_ARCHITECT
+ )
+
+ assert [entry.artifact.title for entry in context.entries] == ["Product Requirements"]
+ assert "Twelve functional requirements." in context.render()
+
+
+async def test_downstream_artifacts_are_excluded(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A later stage's output must not contaminate an earlier decision."""
+ await add_artifact(
+ memory,
+ project,
+ title="Requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="upstream",
+ )
+ await add_artifact(
+ memory,
+ project,
+ title="Test Plan",
+ stage=LifecycleStage.TESTING,
+ body="downstream",
+ artifact_type=ArtifactType.TEST_PLAN,
+ role=AgentRole.QA_ENGINEER,
+ )
+
+ context = await builder(memory).build(
+ project.id, stage=LifecycleStage.ARCHITECTURE, role=AgentRole.SOFTWARE_ARCHITECT
+ )
+
+ titles = [entry.artifact.title for entry in context.entries]
+ assert titles == ["Requirements"]
+
+
+async def test_empty_artifacts_are_skipped(memory: SqlSharedMemory, project: Project) -> None:
+ """An artifact with no version yet has nothing to contribute."""
+ await memory.artifacts.create(
+ Artifact(
+ project_id=project.id,
+ type=ArtifactType.PRD,
+ title="Not yet written",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ owner_role=AgentRole.PRODUCT_MANAGER,
+ )
+ )
+
+ context = await builder(memory).build(
+ project.id, stage=LifecycleStage.ARCHITECTURE, role=AgentRole.SOFTWARE_ARCHITECT
+ )
+
+ assert context.entries == []
+
+
+async def test_approved_artifacts_outrank_drafts(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Agents should reason over what a human sanctioned, first."""
+ await add_artifact(
+ memory,
+ project,
+ title="Draft requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="draft",
+ status=ArtifactStatus.DRAFT,
+ )
+ await add_artifact(
+ memory,
+ project,
+ title="Approved requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="approved",
+ status=ArtifactStatus.APPROVED,
+ )
+
+ context = await builder(memory).build(
+ project.id, stage=LifecycleStage.ARCHITECTURE, role=AgentRole.SOFTWARE_ARCHITECT
+ )
+
+ assert context.entries[0].artifact.title == "Approved requirements"
+
+
+async def test_type_filter_narrows_the_view(memory: SqlSharedMemory, project: Project) -> None:
+ await add_artifact(
+ memory,
+ project,
+ title="Requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="reqs",
+ artifact_type=ArtifactType.PRD,
+ )
+ await add_artifact(
+ memory,
+ project,
+ title="Acceptance Criteria",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="criteria",
+ artifact_type=ArtifactType.ACCEPTANCE_CRITERIA,
+ )
+
+ context = await builder(memory).build(
+ project.id,
+ stage=LifecycleStage.TESTING,
+ role=AgentRole.QA_ENGINEER,
+ include_types={ArtifactType.ACCEPTANCE_CRITERIA},
+ )
+
+ assert [entry.artifact.title for entry in context.entries] == ["Acceptance Criteria"]
+
+
+async def test_budget_truncates_an_oversized_artifact(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ await add_artifact(
+ memory,
+ project,
+ title="Enormous requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="x" * (1000 * CHARS_PER_TOKEN),
+ )
+
+ context = await builder(memory, token_budget=500).build(
+ project.id, stage=LifecycleStage.ARCHITECTURE, role=AgentRole.SOFTWARE_ARCHITECT
+ )
+
+ assert context.entries[0].included_fully is False
+ assert context.estimated_tokens <= 500
+ assert "truncated" in context.render()
+
+
+async def test_omitted_artifacts_are_reported_not_hidden(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A thin answer must be explainable by what the agent was not shown."""
+ await add_artifact(
+ memory,
+ project,
+ title="Approved and large",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="x" * (400 * CHARS_PER_TOKEN),
+ status=ArtifactStatus.APPROVED,
+ )
+ await add_artifact(
+ memory,
+ project,
+ title="Dropped draft",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="y" * (400 * CHARS_PER_TOKEN),
+ status=ArtifactStatus.DRAFT,
+ )
+
+ context = await builder(memory, token_budget=450).build(
+ project.id, stage=LifecycleStage.ARCHITECTURE, role=AgentRole.SOFTWARE_ARCHITECT
+ )
+
+ assert "Dropped draft" in context.omitted
+ assert "Dropped draft" in context.render()
+
+
+async def test_context_exposes_input_artifact_ids_for_traceability(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """These become the upstream half of the trace edges the agent will write."""
+ artifact = await add_artifact(
+ memory,
+ project,
+ title="Requirements",
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ body="reqs",
+ )
+
+ context = await builder(memory).build(
+ project.id, stage=LifecycleStage.ARCHITECTURE, role=AgentRole.SOFTWARE_ARCHITECT
+ )
+
+ assert context.artifact_ids == [artifact.id]
+
+
+async def test_render_includes_the_project_brief(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """The user's own description travels with every agent invocation."""
+ rendered = (
+ await builder(memory).build(
+ project.id,
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ role=AgentRole.PRODUCT_MANAGER,
+ )
+ ).render()
+
+ assert "Hospital System" in rendered
+ assert "Patients, appointments, billing." in rendered
diff --git a/submissions/Victorious/apps/api/tests/test_dependencies.py b/submissions/Victorious/apps/api/tests/test_dependencies.py
new file mode 100644
index 00000000..8a926f33
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_dependencies.py
@@ -0,0 +1,194 @@
+"""Stage readiness rules — pure, no database."""
+
+from __future__ import annotations
+
+from app.domain.approvals import ApprovalKind, ApprovalRequest, ApprovalStatus
+from app.domain.artifacts import Artifact, ArtifactType
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.orchestration.dependencies import (
+ STAGE_GATES,
+ STAGE_INPUTS,
+ ProjectSnapshot,
+ ReadinessStatus,
+ evaluate_readiness,
+ gated_artifact_ids,
+)
+
+PROJECT = "prj_test"
+
+
+def artifact(
+ artifact_type: ArtifactType,
+ *,
+ stage: LifecycleStage = LifecycleStage.REQUIREMENT_DISCOVERY,
+ version: int = 1,
+) -> Artifact:
+ return Artifact(
+ project_id=PROJECT,
+ type=artifact_type,
+ title=artifact_type.value,
+ stage=stage,
+ owner_role=AgentRole.PRODUCT_MANAGER,
+ current_version=version,
+ )
+
+
+def approval(
+ kind: ApprovalKind,
+ status: ApprovalStatus,
+ *,
+ feedback: str | None = None,
+) -> ApprovalRequest:
+ return ApprovalRequest(
+ project_id=PROJECT,
+ kind=kind,
+ stage=LifecycleStage.ARCHITECTURE,
+ title=f"Approve {kind.value}",
+ what_changed="...",
+ why="...",
+ requested_by=AgentRole.EXECUTIVE,
+ status=status,
+ feedback=feedback,
+ )
+
+
+# --- Inputs -------------------------------------------------------------------
+
+
+def test_first_stages_require_nothing() -> None:
+ """07_System_Architecture.md: a project starts from a name and description."""
+ empty = ProjectSnapshot()
+
+ assert evaluate_readiness(LifecycleStage.REQUIREMENT_DISCOVERY, empty).is_ready
+
+
+def test_missing_inputs_are_named() -> None:
+ readiness = evaluate_readiness(LifecycleStage.BUSINESS_VALIDATION, ProjectSnapshot())
+
+ assert readiness.status is ReadinessStatus.MISSING_INPUTS
+ assert ArtifactType.PRD in readiness.missing_inputs
+ assert "prd" in readiness.detail
+
+
+def test_stage_becomes_ready_once_inputs_exist() -> None:
+ snapshot = ProjectSnapshot(artifacts=[artifact(ArtifactType.PRD)])
+
+ assert evaluate_readiness(LifecycleStage.BUSINESS_VALIDATION, snapshot).is_ready
+
+
+def test_artifacts_without_content_do_not_satisfy_inputs() -> None:
+ """A created-but-empty artifact is not upstream work."""
+ snapshot = ProjectSnapshot(artifacts=[artifact(ArtifactType.PRD, version=0)])
+
+ readiness = evaluate_readiness(LifecycleStage.BUSINESS_VALIDATION, snapshot)
+
+ assert readiness.status is ReadinessStatus.MISSING_INPUTS
+
+
+def test_inputs_are_checked_before_gates() -> None:
+ """Asking a human to approve requirements that do not exist is meaningless."""
+ readiness = evaluate_readiness(LifecycleStage.ARCHITECTURE, ProjectSnapshot())
+
+ assert readiness.status is ReadinessStatus.MISSING_INPUTS
+
+
+# --- Gates --------------------------------------------------------------------
+
+
+def test_gate_is_requested_when_no_approval_exists() -> None:
+ snapshot = ProjectSnapshot(
+ artifacts=[artifact(ArtifactType.PRD), artifact(ArtifactType.BUSINESS_ANALYSIS)]
+ )
+
+ readiness = evaluate_readiness(LifecycleStage.ARCHITECTURE, snapshot)
+
+ assert readiness.status is ReadinessStatus.APPROVAL_REQUIRED
+ assert readiness.gate is ApprovalKind.REQUIREMENTS
+
+
+def test_pending_approval_blocks_the_stage() -> None:
+ snapshot = ProjectSnapshot(
+ artifacts=[artifact(ArtifactType.PRD), artifact(ArtifactType.BUSINESS_ANALYSIS)],
+ approvals=[approval(ApprovalKind.REQUIREMENTS, ApprovalStatus.PENDING)],
+ )
+
+ readiness = evaluate_readiness(LifecycleStage.ARCHITECTURE, snapshot)
+
+ assert readiness.status is ReadinessStatus.AWAITING_APPROVAL
+ assert readiness.approval_id is not None
+
+
+def test_granted_approval_unblocks_the_stage() -> None:
+ snapshot = ProjectSnapshot(
+ artifacts=[artifact(ArtifactType.PRD), artifact(ArtifactType.BUSINESS_ANALYSIS)],
+ approvals=[approval(ApprovalKind.REQUIREMENTS, ApprovalStatus.APPROVED)],
+ )
+
+ assert evaluate_readiness(LifecycleStage.ARCHITECTURE, snapshot).is_ready
+
+
+def test_rejection_blocks_and_carries_the_feedback() -> None:
+ snapshot = ProjectSnapshot(
+ artifacts=[artifact(ArtifactType.PRD), artifact(ArtifactType.BUSINESS_ANALYSIS)],
+ approvals=[
+ approval(
+ ApprovalKind.REQUIREMENTS,
+ ApprovalStatus.CHANGES_REQUESTED,
+ feedback="Billing scope is unclear.",
+ )
+ ],
+ )
+
+ readiness = evaluate_readiness(LifecycleStage.ARCHITECTURE, snapshot)
+
+ assert readiness.status is ReadinessStatus.BLOCKED_BY_REJECTION
+ assert readiness.detail == "Billing scope is unclear."
+
+
+def test_latest_approval_of_a_kind_wins() -> None:
+ """A re-raised gate must not be decided by a superseded request."""
+ older = approval(ApprovalKind.REQUIREMENTS, ApprovalStatus.CHANGES_REQUESTED)
+ newer = approval(ApprovalKind.REQUIREMENTS, ApprovalStatus.APPROVED)
+ newer.created_at = older.created_at.replace(year=older.created_at.year + 1)
+
+ snapshot = ProjectSnapshot(
+ artifacts=[artifact(ArtifactType.PRD), artifact(ArtifactType.BUSINESS_ANALYSIS)],
+ approvals=[older, newer],
+ )
+
+ assert evaluate_readiness(LifecycleStage.ARCHITECTURE, snapshot).is_ready
+
+
+# --- Specification conformance ------------------------------------------------
+
+
+def test_gates_cover_the_structural_approvals_the_mvp_requires() -> None:
+ """09_MVP_Roadmap.md: requirements, architecture, and final code generation.
+
+ Technology Stack and Major Engineering Decisions are not stage-shaped and are
+ raised by agents through `requires_approval` instead.
+ """
+ assert set(STAGE_GATES.values()) == {
+ ApprovalKind.REQUIREMENTS,
+ ApprovalKind.ARCHITECTURE,
+ ApprovalKind.CODE_GENERATION,
+ }
+
+
+def test_every_lifecycle_stage_has_declared_inputs() -> None:
+ """A stage missing from STAGE_INPUTS would silently be treated as ready."""
+ assert set(STAGE_INPUTS) == set(LifecycleStage)
+
+
+def test_gated_artifacts_are_the_ones_the_stage_consumes() -> None:
+ """A reviewer sees what the next stage builds on, not the whole project."""
+ prd = artifact(ArtifactType.PRD)
+ analysis = artifact(ArtifactType.BUSINESS_ANALYSIS)
+ unrelated = artifact(ArtifactType.TEST_PLAN, stage=LifecycleStage.TESTING)
+
+ ids = gated_artifact_ids(
+ LifecycleStage.ARCHITECTURE,
+ ProjectSnapshot(artifacts=[prd, analysis, unrelated]),
+ )
+
+ assert set(ids) == {prd.id, analysis.id}
diff --git a/submissions/Victorious/apps/api/tests/test_errors.py b/submissions/Victorious/apps/api/tests/test_errors.py
new file mode 100644
index 00000000..6e201688
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_errors.py
@@ -0,0 +1,102 @@
+"""Error envelope and domain-to-HTTP mapping."""
+
+from __future__ import annotations
+
+import pytest
+from fastapi import FastAPI
+from httpx import ASGITransport, AsyncClient
+
+from app.core.errors import register_exception_handlers
+from app.domain.errors import (
+ ApprovalRequiredError,
+ ConflictError,
+ DependencyNotSatisfiedError,
+ NotFoundError,
+ ProviderError,
+ ValidationError,
+ VictoriousError,
+)
+
+
+@pytest.fixture
+def error_app() -> FastAPI:
+ """Minimal app whose only job is to raise a chosen error."""
+ app = FastAPI()
+ register_exception_handlers(app)
+
+ @app.get("/raise/{error_name}")
+ async def raise_error(error_name: str) -> None:
+ errors: dict[str, VictoriousError] = {
+ "not_found": NotFoundError("Project not found", details={"id": "p-1"}),
+ "validation": ValidationError("Priority must be one of MUST/SHOULD/COULD"),
+ "conflict": ConflictError("Artifact was superseded concurrently"),
+ "dependency": DependencyNotSatisfiedError(
+ "Architecture requires approved requirements",
+ details={"missing": ["requirements"]},
+ ),
+ "approval": ApprovalRequiredError("Technology selection needs sign-off"),
+ "provider": ProviderError("Upstream model unavailable"),
+ "unexpected": None, # type: ignore[dict-item]
+ }
+ if error_name == "unexpected":
+ raise RuntimeError("boom")
+ raise errors[error_name]
+
+ return app
+
+
+@pytest.fixture
+async def error_client(error_app: FastAPI):
+ async with AsyncClient(
+ transport=ASGITransport(app=error_app, raise_app_exceptions=False),
+ base_url="http://test",
+ ) as client:
+ yield client
+
+
+@pytest.mark.parametrize(
+ ("path", "expected_status", "expected_code"),
+ [
+ ("not_found", 404, "not_found"),
+ ("validation", 422, "validation_error"),
+ ("conflict", 409, "conflict"),
+ ("dependency", 409, "dependency_not_satisfied"),
+ ("approval", 403, "approval_required"),
+ ("provider", 502, "provider_error"),
+ ],
+)
+async def test_domain_errors_map_to_expected_status(
+ error_client: AsyncClient, path: str, expected_status: int, expected_code: str
+) -> None:
+ response = await error_client.get(f"/raise/{path}")
+
+ assert response.status_code == expected_status
+ assert response.json()["error"]["code"] == expected_code
+
+
+async def test_error_details_are_preserved(error_client: AsyncClient) -> None:
+ """Structured detail must survive to the client — the UI renders it."""
+ response = await error_client.get("/raise/dependency")
+
+ assert response.json()["error"]["details"] == {"missing": ["requirements"]}
+
+
+async def test_unexpected_errors_do_not_leak_internals(
+ error_client: AsyncClient,
+) -> None:
+ """A bare exception must not expose its message to the caller."""
+ response = await error_client.get("/raise/unexpected")
+
+ assert response.status_code == 500
+ error = response.json()["error"]
+ assert error["code"] == "internal_error"
+ assert "boom" not in error["message"]
+
+
+async def test_routing_404_uses_the_same_envelope(error_client: AsyncClient) -> None:
+ """Every non-2xx response shares one shape, including framework errors."""
+ response = await error_client.get("/no-such-route")
+
+ assert response.status_code == 404
+ assert "error" in response.json()
+ assert response.json()["error"]["code"] == "http_404"
diff --git a/submissions/Victorious/apps/api/tests/test_event_bus.py b/submissions/Victorious/apps/api/tests/test_event_bus.py
new file mode 100644
index 00000000..b4a9f5e8
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_event_bus.py
@@ -0,0 +1,143 @@
+"""Event bus: durable append plus live fan-out."""
+
+from __future__ import annotations
+
+import asyncio
+from collections.abc import AsyncIterator
+
+import pytest_asyncio
+
+from app.core.config import DatabaseSettings
+from app.db.session import Database
+from app.domain.events import EventType, ProjectEvent
+from app.domain.projects import Project
+from app.events.bus import EventBus
+from app.memory.sql_repository import SqlSharedMemory
+
+
+@pytest_asyncio.fixture
+async def memory() -> AsyncIterator[SqlSharedMemory]:
+ database = Database(
+ DatabaseSettings(url="sqlite+aiosqlite:///file:busdb?mode=memory&cache=shared&uri=true")
+ )
+ await database.create_schema()
+ try:
+ yield SqlSharedMemory(database)
+ finally:
+ await database.aclose()
+
+
+@pytest_asyncio.fixture
+async def project(memory: SqlSharedMemory) -> Project:
+ return await memory.projects.create(Project(name="Demo", description="A demo project."))
+
+
+def event(project_id: str, summary: str = "Agent completed") -> ProjectEvent:
+ return ProjectEvent(
+ project_id=project_id, type=EventType.AGENT_COMPLETED, summary=summary
+ )
+
+
+async def test_published_events_are_persisted(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Durability first: the timeline must agree with what the user watched."""
+ bus = EventBus(memory.events)
+
+ await bus.publish(event(project.id, "Product Manager finished"))
+
+ stored = await memory.events.list_for_project(project.id)
+ assert [e.summary for e in stored] == ["Product Manager finished"]
+
+
+async def test_subscriber_receives_live_events(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ bus = EventBus(memory.events)
+
+ async with bus.subscribe(project.id) as queue:
+ await bus.publish(event(project.id, "Architect started"))
+
+ received = await asyncio.wait_for(queue.get(), timeout=2)
+
+ assert received.summary == "Architect started"
+
+
+async def test_subscribers_are_scoped_to_their_project(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ other = await memory.projects.create(Project(name="Other", description="Unrelated."))
+ bus = EventBus(memory.events)
+
+ async with bus.subscribe(project.id) as queue:
+ await bus.publish(event(other.id, "Unrelated activity"))
+
+ assert queue.empty()
+
+
+async def test_multiple_subscribers_each_receive_the_event(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Two browser tabs on the same project both see agent activity."""
+ bus = EventBus(memory.events)
+
+ async with bus.subscribe(project.id) as first, bus.subscribe(project.id) as second:
+ await bus.publish(event(project.id, "Stage completed"))
+
+ assert (await asyncio.wait_for(first.get(), timeout=2)).summary == "Stage completed"
+ assert (await asyncio.wait_for(second.get(), timeout=2)).summary == "Stage completed"
+
+
+async def test_subscriber_is_unregistered_on_exit(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A closed browser tab must not leak a queue the publisher keeps filling."""
+ bus = EventBus(memory.events)
+
+ async with bus.subscribe(project.id):
+ assert bus.subscriber_count(project.id) == 1
+
+ assert bus.subscriber_count(project.id) == 0
+
+
+async def test_subscriber_is_unregistered_when_the_block_raises(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A client disconnecting mid-stream raises; cleanup must still happen."""
+ bus = EventBus(memory.events)
+
+ try:
+ async with bus.subscribe(project.id):
+ raise ConnectionResetError("client went away")
+ except ConnectionResetError:
+ pass
+
+ assert bus.subscriber_count(project.id) == 0
+
+
+async def test_publication_succeeds_with_no_subscribers(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Agent work is not contingent on anyone watching."""
+ bus = EventBus(memory.events)
+
+ published = await bus.publish(event(project.id))
+
+ assert published.id.startswith("evt_")
+ assert len(await memory.events.list_for_project(project.id)) == 1
+
+
+async def test_slow_subscriber_does_not_block_publication(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A stalled consumer drops its oldest events rather than stalling the platform."""
+ bus = EventBus(memory.events)
+ total = 400
+
+ async with bus.subscribe(project.id) as queue:
+ for index in range(total):
+ await bus.publish(event(project.id, f"event {index}"))
+
+ # Every event reached durable storage regardless of the queue overflowing.
+ assert len(await memory.events.list_for_project(project.id, limit=1000)) == total
+ assert queue.qsize() <= 256
diff --git a/submissions/Victorious/apps/api/tests/test_health.py b/submissions/Victorious/apps/api/tests/test_health.py
new file mode 100644
index 00000000..b23675ff
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_health.py
@@ -0,0 +1,103 @@
+"""Health endpoint behaviour."""
+
+from __future__ import annotations
+
+from httpx import AsyncClient
+
+from app.core.health import ComponentHealth, HealthStatus
+from app.core.middleware import CORRELATION_HEADER
+
+
+async def test_liveness_reports_healthy(client: AsyncClient) -> None:
+ response = await client.get("/health")
+
+ assert response.status_code == 200
+ body = response.json()
+ assert body["status"] == HealthStatus.HEALTHY.value
+ assert body["service"] == "Project Victorious"
+ assert body["environment"] == "test"
+
+
+async def test_readiness_reports_registered_components(client: AsyncClient) -> None:
+ """Every component that registers a check appears, with no change here.
+
+ ``shared_memory`` arrived in Milestone 1 by registering itself in the
+ composition root — the readiness endpoint required no modification. Later
+ milestones' components join the same way.
+ """
+ response = await client.get("/health/ready")
+
+ assert response.status_code == 200
+ body = response.json()
+ assert body["status"] == HealthStatus.HEALTHY.value
+
+ components = {component["name"]: component for component in body["components"]}
+ assert {"api", "shared_memory"} <= set(components)
+ assert components["shared_memory"]["status"] == HealthStatus.HEALTHY.value
+ assert all(component["latency_ms"] >= 0 for component in components.values())
+
+
+async def test_readiness_returns_503_when_critical_component_fails(
+ client: AsyncClient,
+) -> None:
+ """A failed critical component must drain traffic, not silently return 200."""
+
+ class FailingCheck:
+ name = "database"
+ critical = True
+
+ async def check(self) -> ComponentHealth:
+ return ComponentHealth(
+ name=self.name,
+ status=HealthStatus.UNHEALTHY,
+ message="connection refused",
+ )
+
+ from app.core.health import HealthRegistry
+
+ registry = client._transport.app.state.container.resolve(HealthRegistry) # type: ignore[union-attr]
+ registry.register(FailingCheck())
+
+ response = await client.get("/health/ready")
+
+ assert response.status_code == 503
+ assert response.json()["status"] == HealthStatus.UNHEALTHY.value
+
+
+async def test_readiness_stays_available_when_noncritical_component_fails(
+ client: AsyncClient,
+) -> None:
+ """Degraded capability still serves traffic — the vector store is optional."""
+
+ class DegradedCheck:
+ name = "vector_store"
+ critical = False
+
+ async def check(self) -> ComponentHealth:
+ return ComponentHealth(
+ name=self.name,
+ status=HealthStatus.UNHEALTHY,
+ message="not provisioned",
+ )
+
+ from app.core.health import HealthRegistry
+
+ registry = client._transport.app.state.container.resolve(HealthRegistry) # type: ignore[union-attr]
+ registry.register(DegradedCheck())
+
+ response = await client.get("/health/ready")
+
+ assert response.status_code == 200
+ assert response.json()["status"] == HealthStatus.DEGRADED.value
+
+
+async def test_correlation_id_is_echoed(client: AsyncClient) -> None:
+ response = await client.get("/health", headers={CORRELATION_HEADER: "trace-me-123"})
+
+ assert response.headers[CORRELATION_HEADER] == "trace-me-123"
+
+
+async def test_correlation_id_is_generated_when_absent(client: AsyncClient) -> None:
+ response = await client.get("/health")
+
+ assert response.headers.get(CORRELATION_HEADER)
diff --git a/submissions/Victorious/apps/api/tests/test_llm.py b/submissions/Victorious/apps/api/tests/test_llm.py
new file mode 100644
index 00000000..68ae3980
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_llm.py
@@ -0,0 +1,310 @@
+"""Provider abstraction: fixtures, recording, retry policy, and registry fallback."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+from pydantic import BaseModel
+
+from app.core.config import LLMProvider as ProviderName
+from app.core.config import LLMSettings
+from app.core.health import HealthStatus
+from app.domain.agents import TokenUsage
+from app.domain.errors import ProviderError
+from app.llm.fixture_provider import FixtureProvider, fixture_name
+from app.llm.provider import (
+ CompletionRequest,
+ CompletionResponse,
+ LLMProvider,
+ Message,
+ Role,
+ StructuredResponse,
+)
+from app.llm.recording import RecordingProvider
+from app.llm.registry import ProviderHealthCheck, build_provider
+from app.llm.retry import (
+ SchemaViolationError,
+ TransientProviderError,
+ backoff_delay,
+ with_retries,
+)
+
+
+class SampleOutput(BaseModel):
+ """Stand-in for an agent output contract."""
+
+ decision: str
+ confidence: float
+
+
+def request(key: str | None = "sample") -> CompletionRequest:
+ return CompletionRequest(
+ system="You are a specialist.",
+ messages=[Message(role=Role.USER, content="Decide something.")],
+ fixture_key=key,
+ )
+
+
+def write_fixture(directory: Path, name: str, payload: dict[str, object]) -> None:
+ directory.mkdir(parents=True, exist_ok=True)
+ (directory / f"{name}.json").write_text(json.dumps(payload), encoding="utf-8")
+
+
+# --- Fixture provider ---------------------------------------------------------
+
+
+async def test_fixture_provider_replays_text(tmp_path: Path) -> None:
+ write_fixture(
+ tmp_path,
+ "sample",
+ {"text": "Recorded answer", "usage": {"input_tokens": 10, "output_tokens": 20}},
+ )
+
+ response = await FixtureProvider(tmp_path).complete(request())
+
+ assert response.text == "Recorded answer"
+ assert response.usage.total == 30
+ assert response.provider == "fixture"
+
+
+async def test_fixture_provider_replays_structured_output(tmp_path: Path) -> None:
+ write_fixture(tmp_path, "sample", {"value": {"decision": "Adopt Postgres", "confidence": 0.9}})
+
+ response = await FixtureProvider(tmp_path).complete_structured(request(), SampleOutput)
+
+ assert response.value.decision == "Adopt Postgres"
+ assert response.value.confidence == 0.9
+
+
+async def test_missing_fixture_explains_how_to_record(tmp_path: Path) -> None:
+ with pytest.raises(ProviderError) as exc_info:
+ await FixtureProvider(tmp_path).complete(request())
+
+ assert "RECORD_FIXTURES" in json.dumps(exc_info.value.details)
+
+
+async def test_stale_fixture_fails_loudly(tmp_path: Path) -> None:
+ """A contract change with an unrefreshed recording must not pass silently."""
+ write_fixture(tmp_path, "sample", {"value": {"decision": "Adopt Postgres"}})
+
+ with pytest.raises(ProviderError, match="no longer matches"):
+ await FixtureProvider(tmp_path).complete_structured(request(), SampleOutput)
+
+
+async def test_fixture_streaming_yields_multiple_chunks(tmp_path: Path) -> None:
+ """The UI's incremental rendering path must be exercised on fixtures too."""
+ write_fixture(tmp_path, "sample", {"text": "x" * 200})
+
+ chunks = [chunk async for chunk in FixtureProvider(tmp_path).stream(request())]
+
+ assert len(chunks) > 1
+ assert "".join(chunks) == "x" * 200
+
+
+def test_fixture_name_prefers_the_explicit_key() -> None:
+ assert fixture_name(request("product_manager.requirement_discovery")) == (
+ "product_manager.requirement_discovery"
+ )
+
+
+def test_fixture_name_falls_back_to_a_stable_hash() -> None:
+ first = fixture_name(request(None))
+ second = fixture_name(request(None))
+
+ assert first == second
+ assert first.startswith("anon_")
+
+
+# --- Recording ----------------------------------------------------------------
+
+
+class StubProvider:
+ """Minimal live-provider stand-in for the recorder."""
+
+ name = "stub"
+ model = "stub-1"
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ return CompletionResponse(
+ text="live answer",
+ usage=TokenUsage(input_tokens=5, output_tokens=7),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ value = schema.model_validate({"decision": "live decision", "confidence": 0.75})
+ return StructuredResponse(
+ value=value,
+ raw_json="{}",
+ usage=TokenUsage(input_tokens=5, output_tokens=7),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def stream(self, request: CompletionRequest): # type: ignore[no-untyped-def]
+ for chunk in ("live ", "stream"):
+ yield chunk
+
+ async def aclose(self) -> None:
+ return None
+
+
+async def test_recorder_writes_a_replayable_fixture(tmp_path: Path) -> None:
+ """The round trip the offline demo depends on."""
+ recorder = RecordingProvider(StubProvider(), tmp_path)
+
+ await recorder.complete_structured(request(), SampleOutput)
+ replayed = await FixtureProvider(tmp_path).complete_structured(request(), SampleOutput)
+
+ assert replayed.value.decision == "live decision"
+ assert replayed.usage.total == 12
+
+
+async def test_recorder_passes_the_response_through(tmp_path: Path) -> None:
+ response = await RecordingProvider(StubProvider(), tmp_path).complete(request())
+
+ assert response.text == "live answer"
+ assert response.provider == "stub"
+
+
+async def test_recording_failure_does_not_break_the_call(tmp_path: Path) -> None:
+ """A recording problem is not an engineering problem."""
+ unwritable = tmp_path / "file-not-a-directory"
+ unwritable.write_text("blocked", encoding="utf-8")
+
+ response = await RecordingProvider(StubProvider(), unwritable).complete(request())
+
+ assert response.text == "live answer"
+
+
+# --- Retry policy -------------------------------------------------------------
+
+
+async def test_transient_failures_are_retried() -> None:
+ attempts = 0
+
+ async def operation(_: int) -> str:
+ nonlocal attempts
+ attempts += 1
+ if attempts < 3:
+ raise TransientProviderError("rate limited")
+ return "succeeded"
+
+ assert await with_retries(operation, max_retries=3, description="test") == "succeeded"
+ assert attempts == 3
+
+
+async def test_schema_violations_receive_the_attempt_number() -> None:
+ """The retry must be able to vary the request, not repeat it identically."""
+ seen: list[int] = []
+
+ async def operation(attempt: int) -> str:
+ seen.append(attempt)
+ if attempt == 0:
+ raise SchemaViolationError("missing field")
+ return "corrected"
+
+ assert await with_retries(operation, max_retries=2, description="test") == "corrected"
+ assert seen == [0, 1]
+
+
+async def test_exhausted_retries_raise_provider_error() -> None:
+ async def operation(_: int) -> str:
+ raise TransientProviderError("still failing")
+
+ with pytest.raises(ProviderError, match="after 3 attempts"):
+ await with_retries(operation, max_retries=2, description="test")
+
+
+async def test_non_retryable_errors_propagate_immediately() -> None:
+ """An authentication failure must not be retried three times."""
+ attempts = 0
+
+ async def operation(_: int) -> str:
+ nonlocal attempts
+ attempts += 1
+ raise ProviderError("invalid api key")
+
+ with pytest.raises(ProviderError, match="invalid api key"):
+ await with_retries(operation, max_retries=3, description="test")
+
+ assert attempts == 1
+
+
+def test_backoff_grows_and_is_jittered() -> None:
+ """Jitter prevents seven agents resynchronising against one rate limit."""
+ assert backoff_delay(0) < backoff_delay(5)
+ assert backoff_delay(99) <= 8.0
+ assert len({backoff_delay(2) for _ in range(20)}) > 1
+
+
+# --- Registry -----------------------------------------------------------------
+
+
+def test_registry_builds_the_fixture_provider(tmp_path: Path) -> None:
+ provider = build_provider(
+ LLMSettings(provider=ProviderName.FIXTURE, fixture_dir=str(tmp_path))
+ )
+
+ assert provider.name == "fixture"
+
+
+def test_registry_falls_back_when_a_key_is_missing(tmp_path: Path) -> None:
+ """Missing credentials must not prevent startup — fixtures still work."""
+ provider = build_provider(
+ LLMSettings(
+ provider=ProviderName.ANTHROPIC, anthropic_api_key=None, fixture_dir=str(tmp_path)
+ )
+ )
+
+ assert provider.name == "fixture"
+
+
+def test_registry_wraps_in_a_recorder_when_enabled(tmp_path: Path) -> None:
+ settings = LLMSettings(
+ provider=ProviderName.ANTHROPIC,
+ anthropic_api_key="sk-test-not-a-real-key",
+ fixture_dir=str(tmp_path),
+ record_fixtures=True,
+ )
+
+ provider = build_provider(settings)
+
+ assert isinstance(provider, RecordingProvider)
+ assert provider.name == "anthropic"
+
+
+async def test_provider_health_reports_degraded_on_fallback(tmp_path: Path) -> None:
+ settings = LLMSettings(provider=ProviderName.ANTHROPIC, fixture_dir=str(tmp_path))
+ provider = build_provider(settings)
+
+ health = await ProviderHealthCheck(provider, settings).check()
+
+ assert health.status is HealthStatus.DEGRADED
+ assert "recorded fixtures" in (health.message or "")
+
+
+async def test_provider_health_is_healthy_when_configured_matches(tmp_path: Path) -> None:
+ settings = LLMSettings(provider=ProviderName.FIXTURE, fixture_dir=str(tmp_path))
+ provider = build_provider(settings)
+
+ health = await ProviderHealthCheck(provider, settings).check()
+
+ assert health.status is HealthStatus.HEALTHY
+
+
+def test_adapters_satisfy_the_provider_protocol(tmp_path: Path) -> None:
+ """Structural conformance, checked without instantiating a network client."""
+ from app.llm.anthropic_provider import AnthropicProvider
+ from app.llm.gemini_provider import GeminiProvider
+
+ for adapter in (FixtureProvider, RecordingProvider, AnthropicProvider, GeminiProvider):
+ for method in ("complete", "complete_structured", "stream", "aclose"):
+ assert callable(getattr(adapter, method)), f"{adapter.__name__}.{method}"
+
+ assert isinstance(FixtureProvider(tmp_path), LLMProvider)
diff --git a/submissions/Victorious/apps/api/tests/test_memory.py b/submissions/Victorious/apps/api/tests/test_memory.py
new file mode 100644
index 00000000..a25dca3c
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_memory.py
@@ -0,0 +1,469 @@
+"""Shared organizational memory against a real database.
+
+These run on SQLite via the same SQLAlchemy layer used in production, so they
+exercise the actual repository implementation rather than a substitute.
+"""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+from itertools import pairwise
+
+import pytest
+import pytest_asyncio
+
+from app.core.config import DatabaseSettings
+from app.db.session import Database
+from app.domain.agents import AgentRun, AgentRunStatus, TokenUsage
+from app.domain.approvals import ApprovalKind, ApprovalRequest, ApprovalStatus
+from app.domain.artifacts import Artifact, ArtifactStatus, ArtifactType, ArtifactVersion
+from app.domain.errors import NotFoundError
+from app.domain.events import EventType, ProjectEvent
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.projects import Project
+from app.domain.traceability import TraceEdge, TraceKind
+from app.memory.sql_repository import SqlSharedMemory
+
+
+@pytest_asyncio.fixture
+async def memory() -> AsyncIterator[SqlSharedMemory]:
+ """A fresh in-memory database per test.
+
+ ``StaticPool`` is not needed: aiosqlite's shared-cache URI keeps one database
+ alive across connections for the duration of the test.
+ """
+ database = Database(
+ DatabaseSettings(url="sqlite+aiosqlite:///file:memdb?mode=memory&cache=shared&uri=true")
+ )
+ await database.create_schema()
+ try:
+ yield SqlSharedMemory(database)
+ finally:
+ await database.aclose()
+
+
+@pytest_asyncio.fixture
+async def project(memory: SqlSharedMemory) -> Project:
+ return await memory.projects.create(
+ Project(name="Hospital Management System", description="Patients, appointments, billing.")
+ )
+
+
+async def make_artifact(
+ memory: SqlSharedMemory,
+ project: Project,
+ *,
+ artifact_type: ArtifactType = ArtifactType.PRD,
+ stage: LifecycleStage = LifecycleStage.REQUIREMENT_DISCOVERY,
+ role: AgentRole = AgentRole.PRODUCT_MANAGER,
+ title: str = "Product Requirements",
+ status: ArtifactStatus = ArtifactStatus.DRAFT,
+) -> Artifact:
+ return await memory.artifacts.create(
+ Artifact(
+ project_id=project.id,
+ type=artifact_type,
+ title=title,
+ stage=stage,
+ owner_role=role,
+ status=status,
+ )
+ )
+
+
+# --- Projects -----------------------------------------------------------------
+
+
+async def test_project_round_trips(memory: SqlSharedMemory, project: Project) -> None:
+ fetched = await memory.projects.get(project.id)
+
+ assert fetched.name == "Hospital Management System"
+ assert fetched.current_stage is LifecycleStage.IDEA
+
+
+async def test_missing_project_raises_not_found(memory: SqlSharedMemory) -> None:
+ with pytest.raises(NotFoundError):
+ await memory.projects.get("prj_missing")
+
+
+async def test_project_stage_advances(memory: SqlSharedMemory, project: Project) -> None:
+ project.current_stage = LifecycleStage.ARCHITECTURE
+ await memory.projects.update(project)
+
+ assert (await memory.projects.get(project.id)).current_stage is LifecycleStage.ARCHITECTURE
+
+
+# --- Artifact versioning ------------------------------------------------------
+
+
+async def test_new_artifact_has_no_content(memory: SqlSharedMemory, project: Project) -> None:
+ artifact = await make_artifact(memory, project)
+
+ assert artifact.current_version == 0
+ assert artifact.has_content is False
+
+
+async def test_appending_a_version_advances_the_artifact(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ artifact = await make_artifact(memory, project)
+
+ await memory.artifacts.append_version(
+ artifact.id,
+ ArtifactVersion(artifact_id=artifact.id, version=1, body_markdown="# PRD v1"),
+ )
+
+ resolved = await memory.artifacts.get_version(artifact.id)
+ assert resolved.artifact.current_version == 1
+ assert resolved.version.body_markdown == "# PRD v1"
+ assert resolved.is_latest
+
+
+async def test_versions_are_append_only(memory: SqlSharedMemory, project: Project) -> None:
+ """The guarantee behind 12_Risk_Analysis.md's version-control mitigation."""
+ artifact = await make_artifact(memory, project)
+
+ await memory.artifacts.append_version(
+ artifact.id, ArtifactVersion(artifact_id=artifact.id, version=1, body_markdown="v1 body")
+ )
+ await memory.artifacts.append_version(
+ artifact.id, ArtifactVersion(artifact_id=artifact.id, version=1, body_markdown="v2 body")
+ )
+
+ versions = await memory.artifacts.list_versions(artifact.id)
+
+ assert [v.version for v in versions] == [1, 2]
+ assert versions[0].body_markdown == "v1 body", "v1 must survive intact"
+ assert versions[1].body_markdown == "v2 body"
+
+
+async def test_version_number_is_assigned_by_the_repository(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A caller-supplied number is ignored, so concurrent writers cannot collide."""
+ artifact = await make_artifact(memory, project)
+
+ stored = await memory.artifacts.append_version(
+ artifact.id,
+ ArtifactVersion(artifact_id=artifact.id, version=99, body_markdown="body"),
+ )
+
+ assert stored.version == 1
+
+
+async def test_historical_version_is_retrievable(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """An agent's decision must remain inspectable as that agent saw it."""
+ artifact = await make_artifact(memory, project)
+ for body in ("v1", "v2", "v3"):
+ await memory.artifacts.append_version(
+ artifact.id, ArtifactVersion(artifact_id=artifact.id, version=1, body_markdown=body)
+ )
+
+ historical = await memory.artifacts.get_version(artifact.id, version=1)
+
+ assert historical.version.body_markdown == "v1"
+ assert historical.is_latest is False
+
+
+async def test_get_version_on_empty_artifact_raises(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ artifact = await make_artifact(memory, project)
+
+ with pytest.raises(NotFoundError, match="no versions"):
+ await memory.artifacts.get_version(artifact.id)
+
+
+async def test_current_versions_returns_the_whole_project(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """One query feeds project-wide staleness detection."""
+ first = await make_artifact(memory, project, title="PRD")
+ second = await make_artifact(
+ memory,
+ project,
+ artifact_type=ArtifactType.SYSTEM_ARCHITECTURE,
+ stage=LifecycleStage.ARCHITECTURE,
+ role=AgentRole.SOFTWARE_ARCHITECT,
+ title="Architecture",
+ )
+
+ await memory.artifacts.append_version(
+ first.id, ArtifactVersion(artifact_id=first.id, version=1, body_markdown="a")
+ )
+ await memory.artifacts.append_version(
+ first.id, ArtifactVersion(artifact_id=first.id, version=1, body_markdown="b")
+ )
+ await memory.artifacts.append_version(
+ second.id, ArtifactVersion(artifact_id=second.id, version=1, body_markdown="c")
+ )
+
+ assert await memory.artifacts.current_versions(project.id) == {first.id: 2, second.id: 1}
+
+
+async def test_artifacts_filter_by_stage(memory: SqlSharedMemory, project: Project) -> None:
+ await make_artifact(memory, project)
+ await make_artifact(
+ memory,
+ project,
+ artifact_type=ArtifactType.SYSTEM_ARCHITECTURE,
+ stage=LifecycleStage.ARCHITECTURE,
+ role=AgentRole.SOFTWARE_ARCHITECT,
+ )
+
+ result = await memory.artifacts.list_for_project(
+ project.id, stage=LifecycleStage.ARCHITECTURE
+ )
+
+ assert [a.type for a in result] == [ArtifactType.SYSTEM_ARCHITECTURE]
+
+
+# --- Traceability through the repository --------------------------------------
+
+
+async def test_staleness_is_detected_end_to_end(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """The Milestone 8 scenario, exercised against real persistence.
+
+ Requirements are revised; the architecture derived from the earlier version
+ is reported stale — with no flag written anywhere.
+ """
+ requirements = await make_artifact(memory, project, title="Requirements")
+ architecture = await make_artifact(
+ memory,
+ project,
+ artifact_type=ArtifactType.SYSTEM_ARCHITECTURE,
+ stage=LifecycleStage.ARCHITECTURE,
+ role=AgentRole.SOFTWARE_ARCHITECT,
+ title="System Architecture",
+ )
+
+ await memory.artifacts.append_version(
+ requirements.id,
+ ArtifactVersion(artifact_id=requirements.id, version=1, body_markdown="reqs v1"),
+ )
+ await memory.artifacts.append_version(
+ architecture.id,
+ ArtifactVersion(artifact_id=architecture.id, version=1, body_markdown="arch v1"),
+ )
+ await memory.traces.add_edge(
+ TraceEdge(
+ project_id=project.id,
+ upstream_artifact_id=requirements.id,
+ downstream_artifact_id=architecture.id,
+ kind=TraceKind.DERIVES_FROM,
+ upstream_version=1,
+ )
+ )
+
+ assert await memory.traces.stale_edges(project.id) == []
+
+ # The user revises requirements.
+ await memory.artifacts.append_version(
+ requirements.id,
+ ArtifactVersion(artifact_id=requirements.id, version=1, body_markdown="reqs v2"),
+ )
+
+ stale = await memory.traces.stale_edges(project.id)
+
+ assert len(stale) == 1
+ assert stale[0].edge.downstream_artifact_id == architecture.id
+ assert stale[0].versions_behind == 1
+
+
+async def test_impact_analysis_reads_the_persisted_graph(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ ids = ["art_reqs", "art_arch", "art_api"]
+ for upstream, downstream in pairwise(ids):
+ await memory.traces.add_edge(
+ TraceEdge(
+ project_id=project.id,
+ upstream_artifact_id=upstream,
+ downstream_artifact_id=downstream,
+ upstream_version=1,
+ )
+ )
+
+ analysis = await memory.traces.analyse_impact(project.id, "art_reqs")
+
+ assert analysis.artifact_ids == ["art_arch", "art_api"]
+
+
+async def test_edges_are_queryable_in_both_directions(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ await memory.traces.add_edge(
+ TraceEdge(
+ project_id=project.id,
+ upstream_artifact_id="art_reqs",
+ downstream_artifact_id="art_arch",
+ upstream_version=1,
+ )
+ )
+
+ downstream = await memory.traces.downstream_of("art_reqs")
+ upstream = await memory.traces.upstream_of("art_arch")
+
+ assert len(downstream) == 1
+ assert len(upstream) == 1
+ assert downstream[0].id == upstream[0].id
+
+
+# --- Agent runs ---------------------------------------------------------------
+
+
+async def test_agent_run_lifecycle(memory: SqlSharedMemory, project: Project) -> None:
+ run = await memory.runs.create(
+ AgentRun(
+ project_id=project.id,
+ role=AgentRole.PRODUCT_MANAGER,
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ task="Draft the PRD",
+ )
+ )
+
+ run.status = AgentRunStatus.COMPLETED
+ run.confidence = 0.82
+ run.reasoning_summary = "Derived twelve requirements from the description."
+ run.token_usage = TokenUsage(input_tokens=1200, output_tokens=3400)
+ await memory.runs.update(run)
+
+ fetched = await memory.runs.get(run.id)
+
+ assert fetched.status is AgentRunStatus.COMPLETED
+ assert fetched.confidence == 0.82
+ assert fetched.token_usage.total == 4600
+
+
+async def test_latest_run_per_role_drives_the_organization_view(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ for task in ("first pass", "revision"):
+ await memory.runs.create(
+ AgentRun(
+ project_id=project.id,
+ role=AgentRole.SOFTWARE_ARCHITECT,
+ stage=LifecycleStage.ARCHITECTURE,
+ task=task,
+ )
+ )
+
+ latest = await memory.runs.latest_for_role(project.id, AgentRole.SOFTWARE_ARCHITECT)
+
+ assert latest is not None
+ assert latest.task == "revision"
+
+
+async def test_latest_run_is_none_for_an_idle_role(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ assert await memory.runs.latest_for_role(project.id, AgentRole.QA_ENGINEER) is None
+
+
+# --- Approvals ----------------------------------------------------------------
+
+
+async def test_approval_decision_is_recorded(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ request = await memory.approvals.create(
+ ApprovalRequest(
+ project_id=project.id,
+ kind=ApprovalKind.TECHNOLOGY_SELECTION,
+ stage=LifecycleStage.ARCHITECTURE,
+ title="Adopt PostgreSQL",
+ what_changed="Selected PostgreSQL over MongoDB.",
+ why="Relational integrity for billing records.",
+ requested_by=AgentRole.SOFTWARE_ARCHITECT,
+ agents_involved=[AgentRole.SOFTWARE_ARCHITECT, AgentRole.BUSINESS_ANALYST],
+ )
+ )
+
+ assert (await memory.approvals.list_pending())[0].id == request.id
+
+ request.status = ApprovalStatus.APPROVED
+ await memory.approvals.update(request)
+
+ assert await memory.approvals.list_pending() == []
+ assert (await memory.approvals.get(request.id)).status.unblocks_progress
+
+
+async def test_rejection_feedback_survives_for_agent_rerun(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Feedback is fed back into agent context, so a rejection teaches."""
+ request = await memory.approvals.create(
+ ApprovalRequest(
+ project_id=project.id,
+ kind=ApprovalKind.ARCHITECTURE,
+ stage=LifecycleStage.ARCHITECTURE,
+ title="Microservice split",
+ what_changed="Proposed seven services.",
+ why="Independent scaling.",
+ requested_by=AgentRole.SOFTWARE_ARCHITECT,
+ )
+ )
+
+ request.status = ApprovalStatus.CHANGES_REQUESTED
+ request.feedback = "Too granular for an MVP — start with a modular monolith."
+ await memory.approvals.update(request)
+
+ fetched = await memory.approvals.get(request.id)
+ assert fetched.feedback is not None
+ assert "modular monolith" in fetched.feedback
+ assert fetched.status.unblocks_progress is False
+
+
+# --- Events -------------------------------------------------------------------
+
+
+async def test_events_are_returned_in_order(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ for index in range(3):
+ await memory.events.append(
+ ProjectEvent(
+ project_id=project.id,
+ type=EventType.AGENT_COMPLETED,
+ summary=f"event {index}",
+ )
+ )
+
+ events = await memory.events.list_for_project(project.id)
+
+ assert [e.summary for e in events] == ["event 0", "event 1", "event 2"]
+
+
+async def test_event_cursor_replays_only_what_was_missed(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Stream resumption: a reconnecting browser must not replay everything."""
+ stored = [
+ await memory.events.append(
+ ProjectEvent(
+ project_id=project.id, type=EventType.AGENT_PROGRESS, summary=f"event {index}"
+ )
+ )
+ for index in range(4)
+ ]
+
+ resumed = await memory.events.list_for_project(project.id, after_id=stored[1].id)
+
+ assert [e.summary for e in resumed] == ["event 2", "event 3"]
+
+
+async def test_unknown_cursor_replays_from_the_start(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A stale cursor must self-heal rather than return an empty stream."""
+ await memory.events.append(
+ ProjectEvent(project_id=project.id, type=EventType.PROJECT_CREATED, summary="created")
+ )
+
+ resumed = await memory.events.list_for_project(project.id, after_id="evt_unknown")
+
+ assert len(resumed) == 1
diff --git a/submissions/Victorious/apps/api/tests/test_orchestration.py b/submissions/Victorious/apps/api/tests/test_orchestration.py
new file mode 100644
index 00000000..57bd3f08
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_orchestration.py
@@ -0,0 +1,691 @@
+"""The engineering workflow, end to end.
+
+These exercise the real path: a real LangGraph traversal, the real Executive AI,
+real `BaseAgent` subclasses, and real persistence. Only the reasoning provider is
+substituted — and it is substituted with one that reads artifact IDs out of the
+rendered context exactly as a language model must, so the traceability contract
+is genuinely exercised rather than bypassed.
+"""
+
+from __future__ import annotations
+
+import re
+from collections.abc import AsyncIterator
+
+import pytest
+import pytest_asyncio
+from pydantic import BaseModel
+
+from app.agents.base import BaseAgent
+from app.agents.contracts import AgentOutput
+from app.core.config import DatabaseSettings, ReviewSettings
+from app.db.session import Database
+from app.domain.approvals import ApprovalKind, ApprovalStatus
+from app.domain.artifacts import ArtifactStatus, ArtifactType, ArtifactVersion
+from app.domain.errors import ProviderError
+from app.domain.events import EventType
+from app.domain.lifecycle import AgentRole, LifecycleStage, StageStatus
+from app.domain.projects import Project
+from app.domain.traceability import TraceKind
+from app.events.bus import EventBus
+from app.llm.provider import CompletionRequest, CompletionResponse, StructuredResponse
+from app.memory.context_builder import ContextBuilder, ProjectContext
+from app.memory.sql_repository import SqlSharedMemory
+from app.orchestration.conflicts import ConflictKind
+from app.orchestration.executive import CoordinationAction
+from app.orchestration.runner import OrchestrationRunner
+from app.review.reviewer import EngineeringReviewer
+
+ARTIFACT_ID_PATTERN = re.compile(r"art_[0-9a-f]{32}")
+
+
+class StageOutput(AgentOutput):
+ """Output contract shared by the test agents."""
+
+
+class ContextAwareProvider:
+ """Produces artifacts that cite whatever upstream the context contained.
+
+ Deliberately parses artifact IDs out of the rendered context, which is what a
+ real model must do to satisfy the orphan guard. A provider that was handed the
+ IDs directly would test the graph while bypassing the contract that makes the
+ traceability graph trustworthy.
+ """
+
+ name = "context_aware"
+ model = "context-aware-1"
+
+ def __init__(self, produces: dict[str, list[tuple[ArtifactType, str]]]) -> None:
+ self._produces = produces
+ self.calls: list[CompletionRequest] = []
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ raise NotImplementedError
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ self.calls.append(request)
+
+ stage = request.metadata.get("stage", "")
+ context_text = " ".join(message.content for message in request.messages)
+ upstream = sorted(set(ARTIFACT_ID_PATTERN.findall(context_text)))
+
+ links = [
+ {
+ "upstream_artifact_id": artifact_id,
+ "kind": TraceKind.DERIVES_FROM.value,
+ "rationale": "Produced from this upstream artifact.",
+ }
+ for artifact_id in upstream
+ ]
+
+ artifacts = [
+ {
+ "type": artifact_type.value,
+ "title": title,
+ "body_markdown": f"# {title}\n\nProduced during {stage}.",
+ "content": {"stage": stage},
+ "summary": f"{title} v1",
+ "derived_from": links,
+ }
+ for artifact_type, title in self._produces.get(stage, [])
+ ]
+
+ from app.domain.agents import TokenUsage
+
+ return StructuredResponse(
+ value=schema.model_validate(
+ {
+ "reasoning": f"Completed {stage} from {len(upstream)} upstream artifact(s).",
+ "confidence": 0.86,
+ "artifacts": artifacts,
+ "concerns": [],
+ "requires_approval": False,
+ "approval_reason": "",
+ }
+ ),
+ raw_json="{}",
+ usage=TokenUsage(input_tokens=50, output_tokens=120),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ yield ""
+
+ async def aclose(self) -> None:
+ return None
+
+
+def make_agent(
+ agent_role: AgentRole, agent_stage: LifecycleStage
+) -> type[BaseAgent[StageOutput]]:
+ """Build a real BaseAgent subclass for a role and stage.
+
+ ``build_task`` is defined in the class body rather than assigned afterwards:
+ ``__abstractmethods__`` is computed at class creation, so a later assignment
+ would leave the class uninstantiable.
+ """
+
+ class _Agent(BaseAgent[StageOutput]):
+ role = agent_role
+ stage = agent_stage
+ output_model = StageOutput
+ prompt_name = "engineering_organization"
+
+ def build_task(self, context: ProjectContext) -> str:
+ return f"Perform {agent_stage.value} for this project."
+
+ _Agent.__name__ = f"{agent_role.value}_agent"
+ return _Agent
+
+
+#: What each stage's agent produces. Types match STAGE_INPUTS so the lifecycle
+#: actually advances rather than stalling on a missing input.
+STAGE_OUTPUTS: dict[str, list[tuple[ArtifactType, str]]] = {
+ LifecycleStage.REQUIREMENT_DISCOVERY.value: [
+ (ArtifactType.PRD, "Product Requirements"),
+ (ArtifactType.ACCEPTANCE_CRITERIA, "Acceptance Criteria"),
+ ],
+ LifecycleStage.BUSINESS_VALIDATION.value: [
+ (ArtifactType.BUSINESS_ANALYSIS, "Business Analysis"),
+ ],
+ LifecycleStage.ARCHITECTURE.value: [
+ (ArtifactType.SYSTEM_ARCHITECTURE, "System Architecture"),
+ ],
+}
+
+
+@pytest_asyncio.fixture
+async def memory() -> AsyncIterator[SqlSharedMemory]:
+ database = Database(
+ DatabaseSettings(url="sqlite+aiosqlite:///file:orchdb?mode=memory&cache=shared&uri=true")
+ )
+ await database.create_schema()
+ try:
+ yield SqlSharedMemory(database)
+ finally:
+ await database.aclose()
+
+
+@pytest_asyncio.fixture
+async def project(memory: SqlSharedMemory) -> Project:
+ return await memory.projects.create(
+ Project(
+ name="Hospital Management System",
+ description="Patients, appointments, billing, doctors, and operations.",
+ )
+ )
+
+
+def build_runner(
+ memory: SqlSharedMemory,
+ *,
+ provider: object | None = None,
+ roles: dict[AgentRole, LifecycleStage] | None = None,
+ review: ReviewSettings | None = None,
+) -> OrchestrationRunner:
+ from app.orchestration.dispatcher import RegistryDispatcher
+
+ resolved_provider = provider or ContextAwareProvider(STAGE_OUTPUTS)
+ events = EventBus(memory.events)
+ context = ContextBuilder(memory.projects, memory.artifacts)
+
+ # `is None` rather than a falsy check: an explicitly empty mapping means "an
+ # organization with no agents", which is a case under test.
+ if roles is None:
+ roles = {
+ AgentRole.PRODUCT_MANAGER: LifecycleStage.REQUIREMENT_DISCOVERY,
+ AgentRole.BUSINESS_ANALYST: LifecycleStage.BUSINESS_VALIDATION,
+ AgentRole.SOFTWARE_ARCHITECT: LifecycleStage.ARCHITECTURE,
+ }
+
+ # The reviewer is attached exactly as `app.core.bootstrap` attaches it, so
+ # these tests exercise the organization as it is actually composed.
+ settings = review or ReviewSettings()
+ reviewer = EngineeringReviewer(resolved_provider, settings) # type: ignore[arg-type]
+
+ dispatcher = RegistryDispatcher()
+ for role, stage in roles.items():
+ agent_class = make_agent(role, stage)
+ dispatcher.register(
+ agent_class(memory, resolved_provider, context, events, reviewer) # type: ignore[arg-type]
+ )
+
+ return OrchestrationRunner(memory, resolved_provider, events, dispatcher, settings) # type: ignore[arg-type]
+
+
+async def approve_latest(memory: SqlSharedMemory, project_id: str) -> None:
+ """Grant the pending approval, as a human would in the Approval Center."""
+ pending = await memory.approvals.list_for_project(project_id, pending_only=True)
+ for request in pending:
+ request.status = ApprovalStatus.APPROVED
+ await memory.approvals.update(request)
+
+
+# --- Lifecycle advance --------------------------------------------------------
+
+
+async def test_workflow_advances_through_the_first_stages(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Idea → Requirement Discovery → Business Validation, then halts at a gate.
+
+ The Architecture stage is gated on requirements approval per
+ 09_MVP_Roadmap.md, so the traversal stops there rather than proceeding.
+ """
+ outcome = await build_runner(memory).advance(project.id)
+
+ assert outcome.executed_stages == [
+ LifecycleStage.REQUIREMENT_DISCOVERY,
+ LifecycleStage.BUSINESS_VALIDATION,
+ ]
+ assert outcome.awaiting_approval
+ assert outcome.pending_approval_id is not None
+
+
+async def test_architecture_proceeds_once_requirements_are_approved(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """The full criterion: Idea → Requirements → Validation → Architecture."""
+ runner = build_runner(memory)
+
+ first = await runner.advance(project.id)
+ assert first.awaiting_approval
+
+ await approve_latest(memory, project.id)
+
+ second = await runner.advance(project.id)
+
+ assert LifecycleStage.ARCHITECTURE in second.executed_stages
+
+ architecture = await memory.artifacts.list_for_project(
+ project.id, artifact_type=ArtifactType.SYSTEM_ARCHITECTURE
+ )
+ assert len(architecture) == 1
+ assert architecture[0].has_content
+
+
+async def test_traversal_is_resumable_across_runner_instances(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """ADR-0009: state lives in shared memory, not a checkpointer.
+
+ A second runner — as a different process would build — resumes correctly
+ because it reads the same memory rather than in-process graph state.
+ """
+ await build_runner(memory).advance(project.id)
+ await approve_latest(memory, project.id)
+
+ resumed = await build_runner(memory).advance(project.id)
+
+ assert LifecycleStage.ARCHITECTURE in resumed.executed_stages
+
+
+# --- Approval gates -----------------------------------------------------------
+
+
+async def test_gate_genuinely_halts_before_downstream_work(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """12_Risk_Analysis.md: a gate that notified while work continued is not a gate."""
+ await build_runner(memory).advance(project.id)
+
+ architecture = await memory.artifacts.list_for_project(
+ project.id, stage=LifecycleStage.ARCHITECTURE
+ )
+ assert architecture == [], "no architecture may exist while requirements await approval"
+
+
+async def test_gate_records_the_five_reviewer_fields(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """10_UI_UX_Plan.md requires: what changed, why, who was involved, impact, actions."""
+ await build_runner(memory).advance(project.id)
+
+ request = (await memory.approvals.list_for_project(project.id, pending_only=True))[0]
+
+ assert request.kind is ApprovalKind.REQUIREMENTS
+ assert request.what_changed
+ assert request.why
+ assert request.requested_by is AgentRole.EXECUTIVE
+ assert request.agents_involved
+ assert request.artifact_ids
+
+
+async def test_rejection_blocks_and_feedback_reaches_the_agent(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A rejection must teach on re-run rather than merely repeat."""
+ provider = ContextAwareProvider(STAGE_OUTPUTS)
+ runner = build_runner(memory, provider=provider)
+
+ await runner.advance(project.id)
+
+ pending = (await memory.approvals.list_for_project(project.id, pending_only=True))[0]
+ pending.status = ApprovalStatus.CHANGES_REQUESTED
+ pending.feedback = "Billing scope is unclear — split it out."
+ await memory.approvals.update(pending)
+
+ blocked = await runner.advance(project.id)
+
+ assert blocked.is_blocked
+ assert "Billing scope is unclear" in blocked.halt_reason
+
+
+async def test_no_second_gate_is_raised_while_one_is_pending(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ runner = build_runner(memory)
+
+ await runner.advance(project.id)
+ second = await runner.advance(project.id)
+
+ assert second.awaiting_approval
+ assert len(await memory.approvals.list_for_project(project.id)) == 1
+
+
+# --- Conflict detection -------------------------------------------------------
+
+
+async def test_stale_derivation_stops_work_and_proposes_resynchronisation(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """A conflicting state is detected rather than silently built upon.
+
+ Stale work is recoverable — the specialists that built on the old version can
+ rebuild against the new one — so the Executive proposes re-synchronisation
+ rather than declaring a dead end. Regenerating approved work is still the
+ user's decision, so it stops at a gate.
+ """
+ runner = build_runner(memory)
+ await runner.advance(project.id)
+ await approve_latest(memory, project.id)
+ await runner.advance(project.id)
+
+ # A human revises the requirements the architecture was derived from.
+ prd = (
+ await memory.artifacts.list_for_project(project.id, artifact_type=ArtifactType.PRD)
+ )[0]
+ await memory.artifacts.append_version(
+ prd.id,
+ ArtifactVersion(artifact_id=prd.id, version=1, body_markdown="# Revised requirements"),
+ )
+
+ outcome = await runner.advance(project.id)
+
+ assert not outcome.made_progress, "no work may proceed on a stale derivation"
+ assert outcome.awaiting_approval
+
+ kinds = {conflict["kind"] for conflict in outcome.conflicts}
+ assert ConflictKind.STALE_DERIVATION.value in kinds
+
+ # Once no other gate is outstanding, the Executive proposes rebuilding the
+ # stale work rather than declaring a dead end.
+ await approve_latest(memory, project.id)
+ decision = await runner.executive.assess(project.id)
+
+ assert decision.action is CoordinationAction.REQUEST_APPROVAL
+ assert decision.gate is ApprovalKind.RESYNCHRONISATION
+
+
+async def test_duplicate_approved_artifacts_halt_the_workflow(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Two approved artifacts of one type is a dead end no rerun resolves."""
+ runner = build_runner(memory)
+ await runner.advance(project.id)
+ # Clear the pending gate so readiness is not what stops the next pass —
+ # the conflict must be what does.
+ await approve_latest(memory, project.id)
+
+ prds = await memory.artifacts.list_for_project(project.id, artifact_type=ArtifactType.PRD)
+ # `approve_latest` decides the request; the artifacts are marked approved by
+ # the Executive, which this test bypasses to isolate the conflict rule.
+ for prd in prds:
+ prd.status = ArtifactStatus.APPROVED
+ await memory.artifacts.update(prd)
+
+ duplicate = await memory.artifacts.create(
+ prds[0].model_copy(update={"id": "art_" + "d" * 32, "status": ArtifactStatus.APPROVED})
+ )
+ await memory.artifacts.append_version(
+ duplicate.id,
+ ArtifactVersion(artifact_id=duplicate.id, version=1, body_markdown="# Competing PRD"),
+ )
+
+ # With no gate outstanding, a conflict no rerun can resolve must stop work.
+ await approve_latest(memory, project.id)
+ decision = await runner.executive.assess(project.id)
+
+ assert decision.action is CoordinationAction.HALT_BLOCKED
+ kinds = {conflict.kind for conflict in decision.conflicts}
+ assert ConflictKind.DUPLICATE_AUTHORITY in kinds
+
+
+# --- Traceability through orchestration ---------------------------------------
+
+
+async def test_orchestrated_agents_build_a_connected_trace_graph(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Every downstream artifact traces back to what it was derived from."""
+ runner = build_runner(memory)
+ await runner.advance(project.id)
+ await approve_latest(memory, project.id)
+ await runner.advance(project.id)
+
+ architecture = (
+ await memory.artifacts.list_for_project(
+ project.id, artifact_type=ArtifactType.SYSTEM_ARCHITECTURE
+ )
+ )[0]
+
+ upstream = await memory.traces.upstream_of(architecture.id)
+ assert upstream, "the architecture must declare what it was derived from"
+
+ prd = (
+ await memory.artifacts.list_for_project(project.id, artifact_type=ArtifactType.PRD)
+ )[0]
+ impact = await memory.traces.analyse_impact(project.id, prd.id)
+ assert architecture.id in impact.artifact_ids
+
+
+# --- Observability ------------------------------------------------------------
+
+
+async def test_every_transition_is_recorded(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ await build_runner(memory).advance(project.id)
+
+ types = {event.type for event in await memory.events.list_for_project(project.id)}
+
+ assert EventType.STAGE_STARTED in types
+ assert EventType.STAGE_COMPLETED in types
+ assert EventType.AGENT_STARTED in types
+ assert EventType.AGENT_COMPLETED in types
+ assert EventType.ARTIFACT_CREATED in types
+ assert EventType.APPROVAL_REQUESTED in types
+
+
+async def test_structured_assignment_is_recorded(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """05_AI_Agent_Architecture.md's structured communication model, made visible."""
+ await build_runner(memory).advance(project.id)
+
+ events = await memory.events.list_for_project(project.id)
+ assignments = [
+ event.payload["assignment"]
+ for event in events
+ if "assignment" in event.payload
+ ]
+
+ assert assignments
+ first = assignments[0]
+ assert isinstance(first, dict)
+ for field in ("sender", "receiver", "task", "required_actions"):
+ assert field in first
+ assert first["sender"] == AgentRole.EXECUTIVE.value
+
+
+async def test_project_stage_state_tracks_progress(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ await build_runner(memory).advance(project.id)
+
+ updated = await memory.projects.get(project.id)
+ completed = set(updated.completed_stages)
+
+ assert LifecycleStage.REQUIREMENT_DISCOVERY in completed
+ assert LifecycleStage.BUSINESS_VALIDATION in completed
+
+ architecture_state = updated.stage_state(LifecycleStage.ARCHITECTURE)
+ assert architecture_state is not None
+ assert architecture_state.status is StageStatus.AWAITING_APPROVAL
+
+
+# --- Failure handling ---------------------------------------------------------
+
+
+async def test_unregistered_stage_halts_with_an_explanation(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """An empty organization must state why it cannot proceed, not silently pass."""
+ runner = build_runner(memory, roles={})
+
+ outcome = await runner.advance(project.id)
+
+ assert outcome.is_blocked
+ assert "No agent is registered" in outcome.halt_reason
+ assert not outcome.made_progress
+
+
+async def test_agent_failure_halts_and_marks_the_stage_blocked(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ class FailingProvider(ContextAwareProvider):
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ raise ProviderError("model unavailable")
+
+ outcome = await build_runner(
+ memory,
+ provider=FailingProvider(STAGE_OUTPUTS),
+ roles={AgentRole.PRODUCT_MANAGER: LifecycleStage.REQUIREMENT_DISCOVERY},
+ ).advance(project.id)
+
+ assert outcome.is_blocked
+ assert outcome.error is not None
+
+ updated = await memory.projects.get(project.id)
+ state = updated.stage_state(LifecycleStage.REQUIREMENT_DISCOVERY)
+ assert state is not None
+ assert state.status is StageStatus.BLOCKED
+
+
+async def test_advancing_an_unknown_project_raises(memory: SqlSharedMemory) -> None:
+ from app.domain.errors import NotFoundError
+
+ with pytest.raises(NotFoundError):
+ await build_runner(memory).advance("prj_missing")
+
+
+# --- Executive boundary -------------------------------------------------------
+
+
+async def test_executive_produces_no_engineering_artifacts(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """15_Development_Guidelines.md: the Executive coordinates, never performs.
+
+ Enforced structurally — ExecutiveAI is not a BaseAgent and has no artifact
+ path — and asserted here so a future change that gave it one would fail.
+ """
+ runner = build_runner(memory)
+ await runner.advance(project.id)
+ await approve_latest(memory, project.id)
+ await runner.advance(project.id)
+
+ artifacts = await memory.artifacts.list_for_project(project.id)
+
+ assert artifacts, "the specialists must have produced work"
+ assert all(artifact.owner_role is not AgentRole.EXECUTIVE for artifact in artifacts)
+
+ runs = await memory.runs.list_for_project(project.id)
+ assert all(run.role is not AgentRole.EXECUTIVE for run in runs)
+
+
+# --- The Executive consults the engineering review ----------------------------
+
+
+async def store_failing_review(
+ memory: SqlSharedMemory, project_id: str, artifact_type: ArtifactType
+) -> None:
+ """Record a below-threshold review against the current version of an artifact."""
+ from app.domain.reviews import ArtifactReview, ReviewVerdict
+
+ artifacts = await memory.artifacts.list_for_project(project_id)
+ artifact = next(item for item in artifacts if item.type is artifact_type)
+
+ await memory.reviews.upsert(
+ ArtifactReview(
+ project_id=project_id,
+ artifact_id=artifact.id,
+ artifact_version=artifact.current_version,
+ stage=artifact.stage,
+ role=artifact.owner_role,
+ quality_score=31,
+ deterministic_score=31,
+ verdict=ReviewVerdict.NEEDS_REVISION,
+ summary="Declares no upstream and carries no structured content.",
+ )
+ )
+
+
+async def test_a_failed_review_does_not_stop_the_organization_by_default(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Advisory by default: a score is a signal to weigh, not an authority to obey."""
+ await build_runner(memory).advance(project.id)
+ await approve_latest(memory, project.id)
+ await store_failing_review(memory, project.id, ArtifactType.PRD)
+
+ outcome = await build_runner(memory).advance(project.id)
+
+ assert not outcome.is_blocked
+ assert LifecycleStage.ARCHITECTURE in outcome.executed_stages
+
+
+async def test_a_failed_review_blocks_the_stage_that_would_consume_it(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Promoted to a gate, the Executive refuses to build on weak upstream work."""
+ await build_runner(memory).advance(project.id)
+ await approve_latest(memory, project.id)
+
+ # Architecture reads the PRD, and the PRD just failed review.
+ await store_failing_review(memory, project.id, ArtifactType.PRD)
+
+ outcome = await build_runner(
+ memory, review=ReviewSettings(blocking=True)
+ ).advance(project.id)
+
+ assert outcome.is_blocked
+ assert "engineering review" in outcome.halt_reason
+ assert "31/100" in outcome.halt_reason
+ assert LifecycleStage.ARCHITECTURE not in outcome.executed_stages
+
+
+async def test_a_weak_artifact_no_stage_reads_does_not_block_anything(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Scoped to a stage's inputs: a weak deployment plan must not stop architecture."""
+ await build_runner(memory).advance(project.id)
+ await approve_latest(memory, project.id)
+
+ # Architecture does not read acceptance criteria; testing does.
+ await store_failing_review(memory, project.id, ArtifactType.ACCEPTANCE_CRITERIA)
+
+ outcome = await build_runner(
+ memory, review=ReviewSettings(blocking=True)
+ ).advance(project.id)
+
+ assert not outcome.is_blocked
+ assert LifecycleStage.ARCHITECTURE in outcome.executed_stages
+
+
+async def test_agents_review_what_they_produce_without_being_asked(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ await build_runner(memory).advance(project.id)
+
+ artifacts = await memory.artifacts.list_for_project(project.id)
+ reviews = await memory.reviews.list_for_project(project.id)
+
+ assert len(reviews) == len([item for item in artifacts if item.has_content])
+ assert all(0 < review.quality_score <= 100 for review in reviews)
+
+
+async def test_a_review_failure_never_costs_the_organization_its_work(
+ memory: SqlSharedMemory, project: Project
+) -> None:
+ """Fail-open: reviewing is a quality signal, not a gate on production."""
+ from app.review.reviewer import EngineeringReviewer
+
+ class ExplodingReviewer(EngineeringReviewer):
+ async def review(self, *args: object, **kwargs: object) -> object:
+ raise RuntimeError("the reviewer fell over")
+
+ runner = build_runner(memory)
+ for agent in runner._dispatcher._agents.values():
+ agent._reviewer = ExplodingReviewer(None, ReviewSettings())
+
+ outcome = await runner.advance(project.id)
+
+ assert outcome.made_progress
+ assert await memory.artifacts.list_for_project(project.id)
+ assert await memory.reviews.list_for_project(project.id) == []
diff --git a/submissions/Victorious/apps/api/tests/test_organization.py b/submissions/Victorious/apps/api/tests/test_organization.py
new file mode 100644
index 00000000..18b25bfc
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_organization.py
@@ -0,0 +1,677 @@
+"""The full engineering organization, end to end.
+
+Runs the `13_Demo_and_Pitch.md` hospital scenario through all nine lifecycle
+stages with the real agents, the real Executive AI, the real workflow graph, and
+real persistence.
+
+The provider is scripted per agent — each factory below is a worked example of
+what that agent's contract expects — but it reads artifact IDs out of the
+rendered context exactly as a live model must, so the traceability contract is
+exercised rather than bypassed.
+"""
+
+from __future__ import annotations
+
+import re
+from collections.abc import AsyncIterator
+from typing import Any
+
+import pytest
+import pytest_asyncio
+from pydantic import BaseModel
+
+from app.agents.organization import AGENT_CLASSES, build_organization
+from app.core.config import DatabaseSettings
+from app.db.session import Database
+from app.domain.agents import TokenUsage
+from app.domain.approvals import ApprovalStatus
+from app.domain.artifacts import ArtifactType
+from app.domain.lifecycle import STAGE_OWNERS, AgentRole, LifecycleStage
+from app.domain.projects import Project
+from app.events.bus import EventBus
+from app.llm.provider import CompletionRequest, CompletionResponse, StructuredResponse
+from app.memory.context_builder import ContextBuilder
+from app.memory.sql_repository import SqlSharedMemory
+from app.orchestration.dispatcher import RegistryDispatcher
+from app.orchestration.runner import OrchestrationRunner
+
+ARTIFACT_ID_PATTERN = re.compile(r"art_[0-9a-f]{32}")
+
+#: The eight artifacts `09_MVP_Roadmap.md` says the MVP must generate.
+REQUIRED_ARTIFACTS = {
+ ArtifactType.PRD,
+ ArtifactType.USER_STORIES,
+ ArtifactType.SYSTEM_ARCHITECTURE,
+ ArtifactType.API_CONTRACT,
+ ArtifactType.DATABASE_SCHEMA,
+ ArtifactType.SOURCE_FILE,
+ ArtifactType.README,
+ ArtifactType.ARCHITECTURE_DOCUMENT,
+}
+
+
+# --- Scripted payloads, one per agent contract --------------------------------
+
+
+def _product_manager() -> dict[str, Any]:
+ return {
+ "objective": "Coordinate patient care, scheduling, and billing in one system.",
+ "target_users": ["Reception staff", "Doctors", "Billing administrators"],
+ "functional_requirements": [
+ {
+ "id": "FR-01",
+ "title": "Register a patient",
+ "description": "Staff can create a patient record with demographics.",
+ "priority": "must",
+ "rationale": "Nothing else in the system works without a patient record.",
+ },
+ {
+ "id": "FR-02",
+ "title": "Book an appointment",
+ "description": "Staff book a patient with a doctor at a time slot.",
+ "priority": "must",
+ "rationale": "Scheduling is the primary daily workflow.",
+ },
+ ],
+ "non_functional_requirements": [
+ {
+ "id": "NFR-01",
+ "title": "Patient data confidentiality",
+ "description": "Records are accessible only to authorised roles.",
+ "priority": "must",
+ "rationale": "Clinical data carries regulatory obligations.",
+ }
+ ],
+ "user_stories": [
+ {
+ "id": "US-01",
+ "as_a": "receptionist",
+ "i_want": "to book an appointment for a patient",
+ "so_that": "the patient is seen by the right doctor",
+ "acceptance_criteria": [
+ "Booking a free slot succeeds and returns a confirmation.",
+ "Booking an already-taken slot is rejected with a conflict error.",
+ ],
+ "requirement_ids": ["FR-02"],
+ "priority": "must",
+ }
+ ],
+ "out_of_scope": ["Insurance claim submission"],
+ "open_questions": ["Which regulatory regime applies to this deployment?"],
+ }
+
+
+def _business_analyst() -> dict[str, Any]:
+ return {
+ "feasibility": "viable_with_changes",
+ "assessment": "The core workflows are sound; access control is underspecified.",
+ "validated_requirement_ids": ["FR-01", "FR-02"],
+ "questioned_requirement_ids": ["NFR-01"],
+ "gaps": [
+ {
+ "area": "Access control",
+ "description": "NFR-01 names no roles or permission model.",
+ "severity": "high",
+ "recommendation": "Define roles before the architecture is designed.",
+ "requirement_ids": ["NFR-01"],
+ }
+ ],
+ "risks": [
+ {
+ "description": "Clinical data handling may require regional certification.",
+ "impact": "high",
+ "likelihood": "possible",
+ "mitigation": "Confirm the applicable regime before go-live.",
+ }
+ ],
+ "opportunities": ["Appointment reminders would reduce no-shows."],
+ }
+
+
+def _architect() -> dict[str, Any]:
+ return {
+ "style": "Modular monolith",
+ "style_rationale": "One team, no independent scaling need; seams allow later split.",
+ "components": [
+ {
+ "name": "patients",
+ "responsibility": "Owns patient records and demographics.",
+ "depends_on": [],
+ "requirement_ids": ["FR-01"],
+ },
+ {
+ "name": "scheduling",
+ "responsibility": "Owns appointments and slot availability.",
+ "depends_on": ["patients"],
+ "requirement_ids": ["FR-02"],
+ },
+ ],
+ "technology_choices": [
+ {
+ "layer": "database",
+ "choice": "PostgreSQL",
+ "alternatives": ["MongoDB"],
+ "rationale": "Appointments need transactional integrity across tables.",
+ "tradeoffs": "Flexible clinical notes need a JSONB column.",
+ }
+ ],
+ "api_endpoints": [
+ {
+ "method": "POST",
+ "path": "/api/v1/appointments",
+ "purpose": "Book an appointment.",
+ "request_summary": "patient_id, doctor_id, slot",
+ "response_summary": "appointment with confirmation code",
+ "requirement_ids": ["FR-02"],
+ }
+ ],
+ "data_entities": [
+ {
+ "name": "patient",
+ "purpose": "A person receiving care.",
+ "fields": [
+ {"name": "id", "type": "uuid", "nullable": False, "description": "PK"},
+ {"name": "name", "type": "text", "nullable": False, "description": ""},
+ ],
+ "relationships": ["one-to-many with appointment"],
+ }
+ ],
+ "scalability_notes": ["Single instance is sufficient at the stated scale."],
+ "security_notes": ["Role-based access on every patient-scoped endpoint."],
+ }
+
+
+def _planner() -> dict[str, Any]:
+ return {
+ "sequencing_rationale": "Data model first; scheduling conflict logic is riskiest.",
+ "tasks": [
+ {
+ "id": "T-01",
+ "title": "Patient schema and migrations",
+ "description": "Create the patient table and its migration.",
+ "component": "patients",
+ "depends_on": [],
+ "requirement_ids": ["FR-01"],
+ "estimate": "half a day",
+ },
+ {
+ "id": "T-02",
+ "title": "Appointment booking with conflict rejection",
+ "description": "Booking endpoint that rejects double-booked slots.",
+ "component": "scheduling",
+ "depends_on": ["T-01"],
+ "requirement_ids": ["FR-02"],
+ "estimate": "one day",
+ },
+ ],
+ "milestones": ["Appointments can be booked and listed through the API."],
+ }
+
+
+def _engineer() -> dict[str, Any]:
+ return {
+ "repository_tree": ["app/", "app/patients/models.py", "app/scheduling/api.py"],
+ "stack_summary": "FastAPI over PostgreSQL, matching the approved decisions.",
+ "files": [
+ {
+ "path": "app/patients/models.py",
+ "language": "python",
+ "purpose": "Patient record model, realising FR-01.",
+ "content": "class Patient:\n id: UUID\n name: str\n",
+ }
+ ],
+ "not_implemented": ["Authentication flows", "Database migrations"],
+ }
+
+
+def _qa() -> dict[str, Any]:
+ return {
+ "strategy": "Cover booking conflicts first — the highest-risk behaviour.",
+ "test_cases": [
+ {
+ "id": "TC-01",
+ "title": "Double booking is rejected",
+ "given": "a slot already booked with a doctor",
+ "when": "a second booking is made for the same slot",
+ "then": "the request is rejected with a conflict error",
+ "kind": "integration",
+ "acceptance_criteria": (
+ "Booking an already-taken slot is rejected with a conflict error."
+ ),
+ "requirement_ids": ["FR-02"],
+ }
+ ],
+ "coverage": [
+ {"requirement_id": "FR-01", "covered": False, "test_case_ids": [],
+ "note": "No acceptance criteria were written for patient registration."},
+ {"requirement_id": "FR-02", "covered": True, "test_case_ids": ["TC-01"], "note": ""},
+ ],
+ "defects": ["The scaffold has no endpoint for FR-01 despite it being a must."],
+ "untestable": ["NFR-01 names no roles, so authorisation cannot be tested."],
+ }
+
+
+def _documentation() -> dict[str, Any]:
+ return {
+ "readme": "# Hospital Management System\n\nScaffold only; not a running system.",
+ "api_documentation": "## POST /api/v1/appointments\n\nBooks an appointment.",
+ "architecture_document": "A modular monolith was chosen because one team owns it.",
+ "developer_guide": "Run migrations before starting the API.",
+ "changelog": "## 0.1.0\n\nInitial scaffold generated.",
+ }
+
+
+def _deployment() -> dict[str, Any]:
+ return {
+ "overview": "Containerised deployment behind a managed PostgreSQL instance.",
+ "checklist": ["Run migrations and confirm the schema version."],
+ "environment_variables": ["DATABASE_URL — PostgreSQL connection string"],
+ "containerisation": "FROM python:3.12-slim",
+ "rollback": ["Redeploy the previous image; column drops are not reversible."],
+ "outstanding": ["Authentication is not implemented.", "FR-01 has no endpoint."],
+ }
+
+
+PAYLOADS: dict[str, dict[str, Any]] = {
+ "product_manager.requirement_discovery": _product_manager(),
+ "business_analyst.business_validation": _business_analyst(),
+ "software_architect.architecture": _architect(),
+ "software_architect.development_planning": _planner(),
+ "full_stack_engineer.implementation": _engineer(),
+ "qa_engineer.testing": _qa(),
+ "documentation.documentation": _documentation(),
+ "documentation.deployment_preparation": _deployment(),
+}
+
+
+class OrganizationProvider:
+ """Returns each agent's scripted payload, citing whatever context it saw."""
+
+ name = "scripted_org"
+ model = "scripted-org-1"
+
+ def __init__(self) -> None:
+ self.seen: list[str] = []
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ raise NotImplementedError
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ key = request.fixture_key or ""
+ self.seen.append(key)
+
+ payload = dict(PAYLOADS.get(key, {}))
+
+ if payload:
+ context_text = " ".join(message.content for message in request.messages)
+ upstream = sorted(set(ARTIFACT_ID_PATTERN.findall(context_text)))
+ payload |= {
+ "reasoning": f"Completed {key} from {len(upstream)} upstream artifact(s).",
+ "confidence": 0.87,
+ "sources": [
+ {
+ "upstream_artifact_id": artifact_id,
+ "kind": "derives_from",
+ "rationale": "Consumed as upstream engineering input.",
+ }
+ for artifact_id in upstream
+ ],
+ "artifacts": [],
+ "concerns": [],
+ "requires_approval": False,
+ "approval_reason": "",
+ }
+ else:
+ # The Executive AI's approval narration, which has its own contract.
+ payload = {
+ "title": "Approve upstream work",
+ "what_changed": "The organization produced artifacts for review.",
+ "why": "The next stage builds directly on them.",
+ }
+
+ return StructuredResponse(
+ value=schema.model_validate(payload),
+ raw_json="{}",
+ usage=TokenUsage(input_tokens=400, output_tokens=900),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ yield ""
+
+ async def aclose(self) -> None:
+ return None
+
+
+# --- Fixtures -----------------------------------------------------------------
+
+
+@pytest_asyncio.fixture
+async def memory() -> AsyncIterator[SqlSharedMemory]:
+ database = Database(
+ DatabaseSettings(url="sqlite+aiosqlite:///file:orgdb?mode=memory&cache=shared&uri=true")
+ )
+ await database.create_schema()
+ try:
+ yield SqlSharedMemory(database)
+ finally:
+ await database.aclose()
+
+
+@pytest_asyncio.fixture
+async def project(memory: SqlSharedMemory) -> Project:
+ return await memory.projects.create(
+ Project(
+ name="Hospital Management System",
+ description=(
+ "A platform for managing patients, appointments, billing, doctors, "
+ "and hospital operations."
+ ),
+ )
+ )
+
+
+@pytest.fixture
+def provider() -> OrganizationProvider:
+ return OrganizationProvider()
+
+
+def build_runner(
+ memory: SqlSharedMemory, provider: OrganizationProvider
+) -> OrchestrationRunner:
+ events = EventBus(memory.events)
+ context = ContextBuilder(memory.projects, memory.artifacts)
+
+ dispatcher = RegistryDispatcher()
+ for agent in build_organization(memory, provider, context, events): # type: ignore[arg-type]
+ dispatcher.register(agent)
+
+ return OrchestrationRunner(memory, provider, events, dispatcher) # type: ignore[arg-type]
+
+
+async def run_lifecycle(
+ memory: SqlSharedMemory, runner: OrchestrationRunner, project_id: str
+) -> list[str]:
+ """Advance to completion, granting each approval as a human would.
+
+ Bounded so a workflow that stops making progress fails the test rather than
+ looping.
+ """
+ executed: list[str] = []
+
+ for _ in range(12):
+ outcome = await runner.advance(project_id)
+ executed.extend(stage.value for stage in outcome.executed_stages)
+
+ if outcome.is_complete:
+ return executed
+
+ if outcome.awaiting_approval:
+ for request in await memory.approvals.list_for_project(
+ project_id, pending_only=True
+ ):
+ request.status = ApprovalStatus.APPROVED
+ await memory.approvals.update(request)
+ continue
+
+ if outcome.is_blocked:
+ raise AssertionError(f"Workflow blocked: {outcome.halt_reason}")
+
+ raise AssertionError("Workflow did not complete within the iteration budget")
+
+
+# --- Organization structure ---------------------------------------------------
+
+
+def test_every_agent_matches_the_domain_owner_of_its_stage() -> None:
+ """A mis-wired organization must be impossible, not merely unlikely."""
+ for agent_class in AGENT_CLASSES:
+ assert STAGE_OWNERS[agent_class.stage] is agent_class.role
+
+
+def test_every_working_stage_has_an_agent() -> None:
+ covered = {agent_class.stage for agent_class in AGENT_CLASSES}
+ expected = {stage for stage in LifecycleStage if stage is not LifecycleStage.IDEA}
+
+ assert covered == expected
+
+
+def test_roster_matches_the_mvp_specification() -> None:
+ """09_MVP_Roadmap.md's roster, minus the Executive AI which coordinates only."""
+ roles = {agent_class.role for agent_class in AGENT_CLASSES}
+
+ assert roles == {
+ AgentRole.PRODUCT_MANAGER,
+ AgentRole.BUSINESS_ANALYST,
+ AgentRole.SOFTWARE_ARCHITECT,
+ AgentRole.FULL_STACK_ENGINEER,
+ AgentRole.QA_ENGINEER,
+ AgentRole.DOCUMENTATION,
+ }
+ assert AgentRole.EXECUTIVE not in roles
+
+
+def test_every_agent_has_its_own_prompt_and_contract() -> None:
+ """05_AI_Agent_Architecture.md: independent modules, not one prompt reused."""
+ from app.agents.prompts import available_prompts
+
+ prompts = set(available_prompts())
+ contracts = {agent_class.output_model for agent_class in AGENT_CLASSES}
+
+ for agent_class in AGENT_CLASSES:
+ assert agent_class.prompt_name in prompts, agent_class.__name__
+
+ # Two agents share the Documentation role but not a contract or a prompt.
+ assert len(contracts) == len(AGENT_CLASSES)
+
+
+def test_dispatcher_rejects_a_role_that_does_not_own_the_stage() -> None:
+ from app.agents.product_manager import ProductManagerAgent
+
+ class Impostor(ProductManagerAgent):
+ role = AgentRole.QA_ENGINEER
+
+ dispatcher = RegistryDispatcher()
+
+ with pytest.raises(ValueError, match="owned by"):
+ dispatcher.register(Impostor(None, None, None, None)) # type: ignore[arg-type]
+
+
+# --- Full lifecycle -----------------------------------------------------------
+
+
+async def test_full_lifecycle_produces_every_required_artifact(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """The completion criterion: all eight artifacts from 09_MVP_Roadmap.md."""
+ executed = await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ assert len(executed) == 8, executed
+
+ produced = {
+ artifact.type for artifact in await memory.artifacts.list_for_project(project.id)
+ }
+ missing = REQUIRED_ARTIFACTS - produced
+ assert not missing, f"missing required artifacts: {sorted(t.value for t in missing)}"
+
+
+async def test_every_agent_ran_exactly_once(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ runs = await memory.runs.list_for_project(project.id)
+ stages = sorted(run.stage.value for run in runs)
+
+ assert len(runs) == 8
+ assert len(set(stages)) == 8
+
+
+async def test_confidence_is_recorded_on_every_run(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """12_Risk_Analysis.md names confidence scoring as a hallucination mitigation."""
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ runs = await memory.runs.list_for_project(project.id)
+
+ assert all(run.confidence is not None for run in runs)
+ assert all(run.reasoning_summary for run in runs)
+ assert all(run.token_usage.total > 0 for run in runs)
+
+
+async def test_no_agent_writes_another_agents_artifacts(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """05_AI_Agent_Architecture.md: agents must not modify each other's state."""
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ for artifact in await memory.artifacts.list_for_project(project.id):
+ assert STAGE_OWNERS[artifact.stage] is artifact.owner_role
+
+
+async def test_every_downstream_artifact_declares_its_upstream(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """The orphan guard, verified across the whole organization."""
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ artifacts = await memory.artifacts.list_for_project(project.id)
+ first_stage = LifecycleStage.REQUIREMENT_DISCOVERY
+
+ for artifact in artifacts:
+ if artifact.stage is first_stage:
+ continue
+ upstream = await memory.traces.upstream_of(artifact.id)
+ assert upstream, f"{artifact.title} declares no upstream"
+
+
+async def test_a_requirement_change_reaches_the_documentation(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """The differentiator, across the full organization.
+
+ Changing the PRD marks artifacts stale all the way to the generated README —
+ which is the question `04_Existing_Solutions.md` says no tool answers.
+ """
+ from app.domain.artifacts import ArtifactVersion
+
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ prd = (
+ await memory.artifacts.list_for_project(project.id, artifact_type=ArtifactType.PRD)
+ )[0]
+ impact = await memory.traces.analyse_impact(project.id, prd.id)
+
+ readme = (
+ await memory.artifacts.list_for_project(project.id, artifact_type=ArtifactType.README)
+ )[0]
+ assert readme.id in impact.artifact_ids, "the README must trace back to the PRD"
+
+ await memory.artifacts.append_version(
+ prd.id,
+ ArtifactVersion(artifact_id=prd.id, version=1, body_markdown="# Revised requirements"),
+ )
+
+ stale_entries = await memory.traces.stale_edges(project.id)
+ stale = {entry.edge.downstream_artifact_id for entry in stale_entries}
+ assert stale, "revising the PRD must make its downstream stale"
+
+
+# --- Rendering ----------------------------------------------------------------
+
+
+async def test_artifacts_render_from_structured_output(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """The document and the structured content cannot disagree — same source."""
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ prd = await memory.artifacts.get_version(
+ (await memory.artifacts.list_for_project(project.id, artifact_type=ArtifactType.PRD))[0].id
+ )
+
+ assert "FR-01" in prd.version.body_markdown
+ assert "Register a patient" in prd.version.body_markdown
+ assert prd.version.content["functional_requirements"][0]["id"] == "FR-01"
+
+
+async def test_architecture_renders_a_component_diagram(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ architecture = await memory.artifacts.get_version(
+ (
+ await memory.artifacts.list_for_project(
+ project.id, artifact_type=ArtifactType.SYSTEM_ARCHITECTURE
+ )
+ )[0].id
+ )
+
+ body = architecture.version.body_markdown
+ assert "```mermaid" in body
+ assert "graph TD" in body
+ assert "scheduling" in body
+
+
+async def test_coverage_report_names_uncovered_requirements(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """Coverage measured against requirements, so a gap is visible as a gap."""
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ coverage = await memory.artifacts.get_version(
+ (
+ await memory.artifacts.list_for_project(
+ project.id, artifact_type=ArtifactType.COVERAGE_REPORT
+ )
+ )[0].id
+ )
+
+ body = coverage.version.body_markdown
+ assert "1 of 2 requirements covered" in body
+ assert "FR-01" in body
+ assert "No acceptance criteria" in body
+
+
+async def test_deployment_plan_lists_variables_without_values(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """A deployment document is where a credential gets committed by accident."""
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ plan = await memory.artifacts.get_version(
+ (
+ await memory.artifacts.list_for_project(
+ project.id, artifact_type=ArtifactType.DEPLOYMENT_PLAN
+ )
+ )[0].id
+ )
+
+ body = plan.version.body_markdown
+ assert "DATABASE_URL" in body
+ assert "PostgreSQL connection string" in body
+ assert "Values belong in a secret store" in body
+
+
+async def test_scaffold_states_what_it_does_not_implement(
+ memory: SqlSharedMemory, project: Project, provider: OrganizationProvider
+) -> None:
+ """ADR-0006: the output must never imply a runnable application."""
+ await run_lifecycle(memory, build_runner(memory, provider), project.id)
+
+ structure = await memory.artifacts.get_version(
+ (
+ await memory.artifacts.list_for_project(
+ project.id, artifact_type=ArtifactType.REPOSITORY_STRUCTURE
+ )
+ )[0].id
+ )
+
+ body = structure.version.body_markdown
+ assert "not a running application" in body
+ assert "Authentication flows" in body
diff --git a/submissions/Victorious/apps/api/tests/test_review.py b/submissions/Victorious/apps/api/tests/test_review.py
new file mode 100644
index 00000000..a39c2c4c
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_review.py
@@ -0,0 +1,344 @@
+"""The engineering review layer.
+
+The properties worth defending here are the ones that make a score mean
+something: it is mostly measured rather than opined, a model cannot overturn a
+measured fact, and a broken reviewer never stops the organization from working.
+"""
+
+from __future__ import annotations
+
+import json
+from collections.abc import AsyncIterator
+
+import pytest
+import pytest_asyncio
+from pydantic import BaseModel
+
+from app.core.config import DatabaseSettings, ReviewSettings
+from app.db.session import Database
+from app.domain.agents import TokenUsage
+from app.domain.artifacts import Artifact, ArtifactType, ArtifactVersion
+from app.domain.lifecycle import AgentRole, LifecycleStage
+from app.domain.projects import Project
+from app.domain.reviews import ReviewVerdict
+from app.llm.provider import CompletionRequest, CompletionResponse, StructuredResponse
+from app.memory.sql_repository import SqlSharedMemory
+from app.review.checks import is_first_stage, run_checks
+from app.review.reviewer import MAX_ADJUSTMENT, EngineeringReviewer
+
+pytestmark = pytest.mark.asyncio
+
+
+class JudgingProvider:
+ """Returns a prepared judgement, recording what it was asked."""
+
+ name = "scripted"
+ model = "scripted-1"
+
+ def __init__(self, payload: dict[str, object]) -> None:
+ self._payload = payload
+ self.requests: list[CompletionRequest] = []
+
+ async def complete(self, request: CompletionRequest) -> CompletionResponse:
+ raise AssertionError("The reviewer must ask for structure, never free text")
+
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ self.requests.append(request)
+ return StructuredResponse(
+ value=schema.model_validate(self._payload),
+ raw_json=json.dumps(self._payload),
+ usage=TokenUsage(input_tokens=10, output_tokens=20),
+ model=self.model,
+ provider=self.name,
+ )
+
+ async def stream(self, request: CompletionRequest) -> AsyncIterator[str]:
+ yield ""
+
+ async def aclose(self) -> None:
+ return None
+
+
+class BrokenProvider(JudgingProvider):
+ async def complete_structured[T: BaseModel](
+ self, request: CompletionRequest, schema: type[T]
+ ) -> StructuredResponse[T]:
+ raise RuntimeError("the reviewing model fell over")
+
+
+def judgement(**overrides: object) -> dict[str, object]:
+ return {
+ "summary": "Solid architecture, weak on failure modes.",
+ "score_adjustment": 0,
+ "strengths": ["Component boundaries follow the requirements."],
+ "weaknesses": ["No failure-mode analysis."],
+ "suggestions": ["Describe what happens when the queue is unavailable."],
+ **overrides,
+ }
+
+
+def artifact_and_version(
+ *,
+ artifact_type: ArtifactType = ArtifactType.SYSTEM_ARCHITECTURE,
+ stage: LifecycleStage = LifecycleStage.ARCHITECTURE,
+ body: str | None = None,
+ content: dict[str, object] | None = None,
+ confidence: float | None = 0.9,
+) -> tuple[Artifact, ArtifactVersion]:
+ artifact = Artifact(
+ project_id="proj-1",
+ type=artifact_type,
+ title="System Architecture",
+ stage=stage,
+ owner_role=AgentRole.SOFTWARE_ARCHITECT,
+ )
+ version = ArtifactVersion(
+ artifact_id=artifact.id,
+ version=1,
+ body_markdown=body if body is not None else "# Architecture\n\n" + "detail. " * 250,
+ content=content if content is not None else {"components": ["api"], "style": "modular"},
+ confidence=confidence,
+ )
+ return artifact, version
+
+
+# --- Deterministic checks -------------------------------------------------
+
+
+async def test_a_complete_artifact_scores_well() -> None:
+ artifact, version = artifact_and_version()
+
+ result = run_checks(artifact, version, upstream_count=2, is_first_stage=False)
+
+ assert result.score >= 90
+ assert result.weaknesses == []
+
+
+async def test_an_artifact_without_upstream_loses_the_traceability_points() -> None:
+ """The property the whole platform rests on, so it is the heaviest check."""
+ artifact, version = artifact_and_version()
+
+ traced = run_checks(artifact, version, upstream_count=1, is_first_stage=False)
+ orphaned = run_checks(artifact, version, upstream_count=0, is_first_stage=False)
+
+ assert traced.score - orphaned.score == 25
+ assert any("no upstream" in finding.text for finding in orphaned.weaknesses)
+
+
+async def test_the_first_stage_is_not_penalised_for_having_no_upstream() -> None:
+ artifact, version = artifact_and_version(
+ artifact_type=ArtifactType.PRD,
+ stage=LifecycleStage.REQUIREMENT_DISCOVERY,
+ content={"functional_requirements": ["FR-01"], "objective": "Book appointments"},
+ )
+
+ result = run_checks(artifact, version, upstream_count=0, is_first_stage=True)
+
+ assert result.score >= 90
+ assert not any("no upstream" in finding.text for finding in result.weaknesses)
+
+
+async def test_requirement_discovery_is_the_only_originating_stage() -> None:
+ assert is_first_stage(LifecycleStage.REQUIREMENT_DISCOVERY)
+ assert not is_first_stage(LifecycleStage.ARCHITECTURE)
+
+
+async def test_a_missing_type_specific_field_is_reported_by_name() -> None:
+ """A finding a user can check beats a finding they have to trust."""
+ artifact, version = artifact_and_version(content={"components": ["api"]})
+
+ result = run_checks(artifact, version, upstream_count=1, is_first_stage=False)
+
+ assert any("style" in finding.text for finding in result.weaknesses)
+
+
+async def test_scores_differ_across_artifacts_of_differing_quality() -> None:
+ """Otherwise the number is theatre — the point of measuring, not opining."""
+ good, good_version = artifact_and_version()
+ thin, thin_version = artifact_and_version(body="# Architecture\n", content={})
+
+ good_result = run_checks(good, good_version, upstream_count=3, is_first_stage=False)
+ thin_result = run_checks(thin, thin_version, upstream_count=0, is_first_stage=False)
+
+ assert good_result.score > thin_result.score + 40
+
+
+async def test_low_confidence_is_penalised_and_routed_to_a_human() -> None:
+ artifact, version = artifact_and_version(confidence=0.2)
+
+ result = run_checks(artifact, version, upstream_count=1, is_first_stage=False)
+
+ assert any("low confidence" in finding.text for finding in result.weaknesses)
+ assert any("human" in finding.text for finding in result.suggestions)
+
+
+# --- Reasoning layer ------------------------------------------------------
+
+
+async def test_reasoning_adjusts_the_score_and_contributes_findings() -> None:
+ provider = JudgingProvider(judgement(score_adjustment=-5))
+ reviewer = EngineeringReviewer(provider, ReviewSettings())
+ artifact, version = artifact_and_version()
+
+ review = await reviewer.review(artifact, version, upstream_count=2)
+
+ assert review.quality_score == review.deterministic_score - 5
+ assert review.reasoning_applied
+ assert review.reviewer_model == "scripted-1"
+ assert any(finding.source == "reasoning" for finding in review.weaknesses)
+ assert any(finding.source == "check" for finding in review.strengths)
+
+
+async def test_reasoning_cannot_rescue_a_structurally_broken_artifact() -> None:
+ """The cap is the design: a model may sharpen a judgement, not overturn one."""
+ provider = JudgingProvider(judgement(score_adjustment=MAX_ADJUSTMENT))
+ reviewer = EngineeringReviewer(provider, ReviewSettings())
+ artifact, version = artifact_and_version(body="# Architecture\n", content={})
+
+ review = await reviewer.review(artifact, version, upstream_count=0)
+
+ assert review.quality_score <= review.deterministic_score + MAX_ADJUSTMENT
+ assert review.verdict is ReviewVerdict.NEEDS_REVISION
+
+
+async def test_an_adjustment_beyond_the_cap_is_rejected_not_clamped() -> None:
+ """Schema-level refusal, so an out-of-range judgement never silently applies."""
+ provider = JudgingProvider(judgement(score_adjustment=MAX_ADJUSTMENT + 40))
+ reviewer = EngineeringReviewer(provider, ReviewSettings())
+ artifact, version = artifact_and_version()
+
+ review = await reviewer.review(artifact, version, upstream_count=2)
+
+ assert not review.reasoning_applied
+ assert review.quality_score == review.deterministic_score
+
+
+async def test_a_broken_reviewer_degrades_to_the_structural_review() -> None:
+ reviewer = EngineeringReviewer(BrokenProvider({}), ReviewSettings())
+ artifact, version = artifact_and_version()
+
+ review = await reviewer.review(artifact, version, upstream_count=2)
+
+ assert review.quality_score == review.deterministic_score
+ assert not review.reasoning_applied
+ assert review.reviewer_model is None
+
+
+async def test_reasoning_can_be_switched_off() -> None:
+ provider = JudgingProvider(judgement())
+ reviewer = EngineeringReviewer(provider, ReviewSettings(use_reasoning=False))
+ artifact, version = artifact_and_version()
+
+ review = await reviewer.review(artifact, version, upstream_count=2)
+
+ assert provider.requests == []
+ assert not review.reasoning_applied
+
+
+async def test_the_fixture_key_is_typed_so_one_recording_covers_every_project() -> None:
+ provider = JudgingProvider(judgement())
+ reviewer = EngineeringReviewer(provider, ReviewSettings())
+ artifact, version = artifact_and_version()
+
+ await reviewer.review(artifact, version, upstream_count=2)
+
+ assert provider.requests[0].fixture_key == "review.system_architecture"
+
+
+async def test_the_reviewing_model_is_shown_the_structural_evidence() -> None:
+ """So its judgement builds on the facts rather than contradicting them."""
+ provider = JudgingProvider(judgement())
+ reviewer = EngineeringReviewer(provider, ReviewSettings())
+ artifact, version = artifact_and_version()
+
+ await reviewer.review(artifact, version, upstream_count=2)
+
+ prompt = provider.requests[0].messages[0].content
+ assert "Structural score:" in prompt
+ assert "Traced to 2 upstream artifact(s)." in prompt
+
+
+@pytest.mark.parametrize(
+ ("score_adjustment", "expected"),
+ [
+ (MAX_ADJUSTMENT, ReviewVerdict.APPROVED),
+ (0, ReviewVerdict.APPROVED_WITH_SUGGESTIONS),
+ ],
+)
+async def test_the_verdict_follows_the_configured_thresholds(
+ score_adjustment: int, expected: ReviewVerdict
+) -> None:
+ provider = JudgingProvider(judgement(score_adjustment=score_adjustment))
+ reviewer = EngineeringReviewer(
+ provider, ReviewSettings(strong_threshold=95, revision_threshold=60)
+ )
+ artifact, version = artifact_and_version(confidence=0.6)
+
+ review = await reviewer.review(artifact, version, upstream_count=2)
+
+ assert review.verdict is expected
+
+
+async def test_only_a_verdict_at_or_above_the_threshold_is_acceptable() -> None:
+ assert ReviewVerdict.APPROVED.is_acceptable
+ assert ReviewVerdict.APPROVED_WITH_SUGGESTIONS.is_acceptable
+ assert not ReviewVerdict.NEEDS_REVISION.is_acceptable
+
+
+# --- Persistence ----------------------------------------------------------
+
+
+@pytest_asyncio.fixture
+async def memory() -> AsyncIterator[SqlSharedMemory]:
+ database = Database(
+ DatabaseSettings(url="sqlite+aiosqlite:///file:reviewdb?mode=memory&cache=shared&uri=true")
+ )
+ await database.create_schema()
+ try:
+ yield SqlSharedMemory(database)
+ finally:
+ await database.aclose()
+
+
+async def test_a_review_is_stored_per_version_and_replaced_in_place(
+ memory: SqlSharedMemory,
+) -> None:
+ """Re-reviewing a version must correct it, not accumulate duplicates."""
+ project = await memory.projects.create(Project(name="Clinic", description="Bookings."))
+ artifact = await memory.artifacts.create(
+ Artifact(
+ project_id=project.id,
+ type=ArtifactType.SYSTEM_ARCHITECTURE,
+ title="System Architecture",
+ stage=LifecycleStage.ARCHITECTURE,
+ owner_role=AgentRole.SOFTWARE_ARCHITECT,
+ )
+ )
+ version = await memory.artifacts.append_version(
+ artifact.id,
+ ArtifactVersion(
+ artifact_id=artifact.id,
+ version=1,
+ body_markdown="# Architecture\n\n" + "detail. " * 250,
+ content={"components": ["api"], "style": "modular"},
+ confidence=0.9,
+ ),
+ )
+
+ reviewer = EngineeringReviewer(JudgingProvider(judgement()), ReviewSettings())
+
+ first = await reviewer.review(artifact, version, upstream_count=2)
+ await memory.reviews.upsert(first)
+
+ second = await reviewer.review(artifact, version, upstream_count=0)
+ await memory.reviews.upsert(second)
+
+ stored = await memory.reviews.list_for_project(project.id)
+ assert len(stored) == 1
+ assert stored[0].quality_score == second.quality_score
+
+ latest = await memory.reviews.for_artifact(artifact.id)
+ assert latest is not None
+ assert latest.artifact_version == 1
diff --git a/submissions/Victorious/apps/api/tests/test_stream.py b/submissions/Victorious/apps/api/tests/test_stream.py
new file mode 100644
index 00000000..c34df75e
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_stream.py
@@ -0,0 +1,299 @@
+"""The live engineering activity stream.
+
+SSE is awkward to test through a client that buffers, so these exercise the
+frame formatting directly and the endpoint through a real streaming request.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import json
+from collections.abc import AsyncIterator
+
+import pytest
+import pytest_asyncio
+from httpx import ASGITransport, AsyncClient
+
+from app.core.config import (
+ DatabaseSettings,
+ Environment,
+ LLMProvider,
+ LLMSettings,
+ ObservabilitySettings,
+ Settings,
+)
+from app.db.session import Database
+from app.domain.events import EventType, ProjectEvent
+from app.events.bus import EventBus
+from app.events.sse import format_event, format_heartbeat, format_open, format_retry
+from app.main import create_app
+from app.memory.repository import SharedMemory
+
+PREFIX = "/api/v1"
+
+
+# --- Frame format -------------------------------------------------------------
+
+
+def make_event(summary: str = "Product Manager started") -> ProjectEvent:
+ return ProjectEvent(
+ project_id="prj_test",
+ type=EventType.AGENT_STARTED,
+ summary=summary,
+ payload={"run_id": "run_1"},
+ )
+
+
+def test_event_frame_carries_id_type_and_data() -> None:
+ """The id is what a reconnecting browser echoes back as Last-Event-ID."""
+ event = make_event()
+
+ frame = format_event(event)
+
+ assert frame.startswith(f"id: {event.id}\n")
+ assert "event: agent_started\n" in frame
+ assert frame.endswith("\n\n")
+
+ data = json.loads(frame.split("data: ", 1)[1].strip())
+ assert data["summary"] == "Product Manager started"
+ assert data["payload"]["run_id"] == "run_1"
+
+
+def test_event_frame_is_a_single_line_of_data() -> None:
+ """A newline inside `data:` would split the frame into two malformed ones."""
+ frame = format_event(make_event("Line one\nline two"))
+
+ data_lines = [line for line in frame.split("\n") if line.startswith("data: ")]
+ assert len(data_lines) == 1
+
+
+def test_heartbeat_is_a_comment_frame() -> None:
+ """Comments keep the socket alive without reaching the application."""
+ assert format_heartbeat().startswith(":")
+ assert format_heartbeat().endswith("\n\n")
+
+
+def test_retry_and_open_frames_are_well_formed() -> None:
+ assert format_retry().startswith("retry: ")
+ assert "event: stream_open" in format_open()
+
+
+# --- Endpoint -----------------------------------------------------------------
+
+
+@pytest_asyncio.fixture
+async def api() -> AsyncIterator[AsyncClient]:
+ settings = Settings(
+ environment=Environment.TEST,
+ database=DatabaseSettings(
+ url="sqlite+aiosqlite:///file:streamdb?mode=memory&cache=shared&uri=true"
+ ),
+ llm=LLMSettings(provider=LLMProvider.FIXTURE),
+ observability=ObservabilitySettings(log_level="ERROR", json_logs=False),
+ )
+ app = create_app(settings)
+
+ async with (
+ AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client,
+ app.router.lifespan_context(app),
+ ):
+ await app.state.container.resolve(Database).create_schema()
+ client.app_ref = app # type: ignore[attr-defined]
+ yield client
+
+
+async def create_project(api: AsyncClient) -> str:
+ response = await api.post(
+ f"{PREFIX}/projects",
+ json={"name": "Hospital System", "description": "Patients and appointments."},
+ )
+ return response.json()["id"]
+
+
+class FakeRequest:
+ """A request that reports disconnection after a set number of checks.
+
+ The stream is deliberately infinite, so an HTTP client would need the server
+ to close it — and an in-process ASGI transport never signals disconnect.
+ Driving the generator directly tests the real logic (replay, handover,
+ heartbeat, dedupe) while remaining bounded.
+ """
+
+ def __init__(self, checks_before_disconnect: int = 1) -> None:
+ self._remaining = checks_before_disconnect
+
+ async def is_disconnected(self) -> bool:
+ if self._remaining <= 0:
+ return True
+ self._remaining -= 1
+ return False
+
+
+async def collect(api: AsyncClient, project_id: str, **kwargs: object) -> str:
+ """Drive the stream generator to completion and return its frames."""
+ from app.api.routers.stream import _stream
+
+ container = api.app_ref.state.container # type: ignore[attr-defined]
+ bus = container.resolve(EventBus)
+ memory = container.resolve(SharedMemory) # type: ignore[type-abstract]
+
+ frames = [
+ frame
+ async for frame in _stream(
+ FakeRequest(kwargs.get("checks", 1)), # type: ignore[arg-type]
+ bus,
+ memory,
+ project_id,
+ kwargs.get("last_event_id"), # type: ignore[arg-type]
+ heartbeat_seconds=0.05,
+ )
+ ]
+ return "".join(frames)
+
+
+async def test_stream_opens_with_retry_and_replays_history(api: AsyncClient) -> None:
+ """A client joining mid-project sees what already happened."""
+ project_id = await create_project(api)
+
+ output = await collect(api, project_id)
+
+ assert output.startswith("retry: ")
+ assert "event: stream_open" in output
+ assert "event: project_created" in output
+
+
+async def test_last_event_id_skips_what_the_client_already_saw(
+ api: AsyncClient,
+) -> None:
+ project_id = await create_project(api)
+ seen = (await api.get(f"{PREFIX}/projects/{project_id}/events")).json()
+
+ output = await collect(api, project_id, last_event_id=seen[-1]["id"])
+
+ assert "event: stream_open" in output
+ assert "event: project_created" not in output
+
+
+async def test_replayed_events_are_not_sent_twice(api: AsyncClient) -> None:
+ """Subscribing before replaying can duplicate; the dedupe set prevents it."""
+ project_id = await create_project(api)
+
+ container = api.app_ref.state.container # type: ignore[attr-defined]
+ bus = container.resolve(EventBus)
+ memory = container.resolve(SharedMemory) # type: ignore[type-abstract]
+
+ # Publish through the bus so the event is both persisted and queued, which is
+ # exactly the handover race the dedupe guards.
+ from app.api.routers.stream import _stream
+
+ async with bus.subscribe(project_id):
+ published = await bus.publish(
+ ProjectEvent(
+ project_id=project_id,
+ type=EventType.AGENT_STARTED,
+ summary="Product Manager started",
+ )
+ )
+
+ frames = [
+ frame
+ async for frame in _stream(
+ FakeRequest(2), # type: ignore[arg-type]
+ bus,
+ memory,
+ project_id,
+ None,
+ heartbeat_seconds=0.05,
+ )
+ ]
+ output = "".join(frames)
+
+ assert output.count(f"id: {published.id}") == 1
+ assert "event: agent_started" in output
+
+
+async def test_stream_disconnects_cleanly(api: AsyncClient) -> None:
+ """A closed browser tab must not leave a subscriber attached to the bus."""
+ project_id = await create_project(api)
+ bus: EventBus = api.app_ref.state.container.resolve(EventBus) # type: ignore[attr-defined]
+
+ await collect(api, project_id)
+
+ assert bus.subscriber_count(project_id) == 0
+
+
+async def test_stream_for_unknown_project_is_a_404(api: AsyncClient) -> None:
+ """Checked before the stream opens, so it is a real status not an error frame."""
+ async with api.stream(
+ "GET", f"{PREFIX}/projects/prj_missing/events/stream"
+ ) as response:
+ assert response.status_code == 404
+
+
+# --- Bus behaviour under streaming --------------------------------------------
+
+
+async def test_publishing_reaches_a_live_subscriber(api: AsyncClient) -> None:
+ """The property the whole view depends on: publisher and stream share a bus."""
+ project_id = await create_project(api)
+ bus: EventBus = api.app_ref.state.container.resolve(EventBus) # type: ignore[attr-defined]
+
+ async with bus.subscribe(project_id) as queue:
+ await bus.publish(
+ ProjectEvent(
+ project_id=project_id,
+ type=EventType.AGENT_COMPLETED,
+ summary="Architect finished",
+ )
+ )
+
+ received = await asyncio.wait_for(queue.get(), timeout=2)
+
+ assert received.summary == "Architect finished"
+
+
+async def test_advancing_publishes_to_a_live_subscriber(api: AsyncClient) -> None:
+ """Running the organization emits activity a stream would carry."""
+ project_id = await create_project(api)
+ bus: EventBus = api.app_ref.state.container.resolve(EventBus) # type: ignore[attr-defined]
+
+ async with bus.subscribe(project_id) as queue:
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+
+ received = []
+ while not queue.empty():
+ received.append(queue.get_nowait())
+
+ types = {event.type for event in received}
+ assert EventType.AGENT_STARTED in types
+ assert EventType.ARTIFACT_CREATED in types
+ assert EventType.AGENT_COMPLETED in types
+
+
+@pytest.mark.parametrize("subscribers", [1, 3])
+async def test_every_open_stream_receives_the_event(
+ api: AsyncClient, subscribers: int
+) -> None:
+ """Several browser tabs on one project all see the same activity."""
+ project_id = await create_project(api)
+ bus: EventBus = api.app_ref.state.container.resolve(EventBus) # type: ignore[attr-defined]
+
+ from contextlib import AsyncExitStack
+
+ async with AsyncExitStack() as stack:
+ queues = [
+ await stack.enter_async_context(bus.subscribe(project_id))
+ for _ in range(subscribers)
+ ]
+
+ await bus.publish(
+ ProjectEvent(
+ project_id=project_id,
+ type=EventType.STAGE_COMPLETED,
+ summary="Architecture completed",
+ )
+ )
+
+ for queue in queues:
+ event = await asyncio.wait_for(queue.get(), timeout=2)
+ assert event.summary == "Architecture completed"
diff --git a/submissions/Victorious/apps/api/tests/test_traceability.py b/submissions/Victorious/apps/api/tests/test_traceability.py
new file mode 100644
index 00000000..a152e202
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_traceability.py
@@ -0,0 +1,203 @@
+"""Traceability: staleness detection and change impact analysis.
+
+Pure domain tests — no database, no event loop. That they need neither is the
+point of ADR-0003's layering: the rules that make this platform different from a
+code generator are testable in isolation.
+"""
+
+from __future__ import annotations
+
+from app.domain.traceability import (
+ TraceEdge,
+ TraceKind,
+ analyse_impact,
+ stale_artifact_ids,
+ stale_edges,
+ upstream_of,
+)
+
+PROJECT = "prj_test"
+
+
+def edge(
+ upstream: str,
+ downstream: str,
+ *,
+ version: int = 1,
+ kind: TraceKind = TraceKind.DERIVES_FROM,
+) -> TraceEdge:
+ return TraceEdge(
+ project_id=PROJECT,
+ upstream_artifact_id=upstream,
+ downstream_artifact_id=downstream,
+ kind=kind,
+ upstream_version=version,
+ )
+
+
+# --- Staleness ----------------------------------------------------------------
+
+
+def test_edge_is_fresh_when_upstream_has_not_moved() -> None:
+ edges = [edge("requirements", "architecture", version=1)]
+
+ assert stale_edges(edges, {"requirements": 1}) == []
+
+
+def test_edge_is_stale_when_upstream_advances() -> None:
+ """The core mechanism: revising requirements makes the architecture stale."""
+ edges = [edge("requirements", "architecture", version=1)]
+
+ result = stale_edges(edges, {"requirements": 2})
+
+ assert len(result) == 1
+ assert result[0].versions_behind == 1
+ assert result[0].edge.downstream_artifact_id == "architecture"
+
+
+def test_versions_behind_counts_multiple_revisions() -> None:
+ edges = [edge("requirements", "architecture", version=1)]
+
+ assert stale_edges(edges, {"requirements": 5})[0].versions_behind == 4
+
+
+def test_unknown_upstream_is_skipped_rather_than_assumed_stale() -> None:
+ """A missing artifact must not manufacture a false staleness alarm."""
+ edges = [edge("ghost", "architecture", version=1)]
+
+ assert stale_edges(edges, {}) == []
+
+
+def test_stale_artifact_ids_deduplicates_across_edges() -> None:
+ """An artifact stale via two upstreams is reported once."""
+ edges = [
+ edge("requirements", "architecture", version=1),
+ edge("business_analysis", "architecture", version=1),
+ ]
+
+ result = stale_artifact_ids(edges, {"requirements": 2, "business_analysis": 3})
+
+ assert result == {"architecture"}
+
+
+# --- Impact analysis ----------------------------------------------------------
+
+
+def test_direct_impact_is_depth_one() -> None:
+ edges = [edge("requirements", "architecture")]
+
+ analysis = analyse_impact("requirements", edges)
+
+ assert analysis.artifact_ids == ["architecture"]
+ assert analysis.impacted[0].depth == 1
+ assert analysis.direct == analysis.impacted
+
+
+def test_impact_is_transitive() -> None:
+ """The question no existing tool answers, per 04_Existing_Solutions.md."""
+ edges = [
+ edge("requirements", "architecture"),
+ edge("architecture", "api_contract"),
+ edge("api_contract", "source_file"),
+ edge("source_file", "test_cases"),
+ ]
+
+ analysis = analyse_impact("requirements", edges)
+
+ assert analysis.artifact_ids == ["architecture", "api_contract", "source_file", "test_cases"]
+ assert [item.depth for item in analysis.impacted] == [1, 2, 3, 4]
+
+
+def test_impact_records_the_path_to_each_artifact() -> None:
+ """The path explains *why* an artifact is affected, not merely that it is."""
+ edges = [
+ edge("requirements", "architecture"),
+ edge("architecture", "source_file"),
+ ]
+
+ analysis = analyse_impact("requirements", edges)
+ source_file = next(i for i in analysis.impacted if i.artifact_id == "source_file")
+
+ assert source_file.path == ["requirements", "architecture", "source_file"]
+
+
+def test_impact_excludes_unrelated_branches() -> None:
+ """Precision matters: over-reporting impact trains users to ignore it."""
+ edges = [
+ edge("requirements", "architecture"),
+ edge("unrelated_doc", "unrelated_child"),
+ ]
+
+ analysis = analyse_impact("requirements", edges)
+
+ assert analysis.artifact_ids == ["architecture"]
+
+
+def test_impact_terminates_on_cyclic_graphs() -> None:
+ """Real project graphs contain feedback loops; traversal must not hang."""
+ edges = [
+ edge("requirements", "architecture"),
+ edge("architecture", "decision"),
+ edge("decision", "requirements"),
+ ]
+
+ analysis = analyse_impact("requirements", edges)
+
+ assert set(analysis.artifact_ids) == {"architecture", "decision"}
+ assert "requirements" not in analysis.artifact_ids
+
+
+def test_impact_reports_shortest_path_when_several_exist() -> None:
+ """Breadth-first: the most direct explanation wins."""
+ edges = [
+ edge("requirements", "architecture"),
+ edge("requirements", "api_contract"),
+ edge("architecture", "api_contract"),
+ ]
+
+ analysis = analyse_impact("requirements", edges)
+ api_contract = next(i for i in analysis.impacted if i.artifact_id == "api_contract")
+
+ assert api_contract.depth == 1
+
+
+def test_max_depth_limits_traversal() -> None:
+ edges = [
+ edge("a", "b"),
+ edge("b", "c"),
+ edge("c", "d"),
+ ]
+
+ analysis = analyse_impact("a", edges, max_depth=2)
+
+ assert analysis.artifact_ids == ["b", "c"]
+
+
+def test_leaf_artifact_has_empty_impact() -> None:
+ analysis = analyse_impact("test_cases", [edge("source_file", "test_cases")])
+
+ assert analysis.is_empty
+
+
+def test_edge_kind_is_preserved_through_analysis() -> None:
+ """Milestone 8 uses the kind to propose proportionate re-synchronisation."""
+ edges = [edge("acceptance_criteria", "test_cases", kind=TraceKind.TESTS)]
+
+ analysis = analyse_impact("acceptance_criteria", edges)
+
+ assert analysis.impacted[0].via_kind is TraceKind.TESTS
+
+
+# --- Reverse traversal --------------------------------------------------------
+
+
+def test_upstream_of_answers_why_this_artifact_exists() -> None:
+ edges = [
+ edge("requirements", "architecture"),
+ edge("business_analysis", "architecture"),
+ edge("architecture", "source_file"),
+ ]
+
+ result = upstream_of("architecture", edges)
+
+ assert {e.upstream_artifact_id for e in result} == {"requirements", "business_analysis"}
diff --git a/submissions/Victorious/apps/api/tests/test_traceability_api.py b/submissions/Victorious/apps/api/tests/test_traceability_api.py
new file mode 100644
index 00000000..521d1a8b
--- /dev/null
+++ b/submissions/Victorious/apps/api/tests/test_traceability_api.py
@@ -0,0 +1,249 @@
+"""The traceability graph and impact preview over HTTP.
+
+`04_Existing_Solutions.md` names these as the questions no tool on the market
+answers. These verify the answers through the endpoints the workspace calls.
+"""
+
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+
+import pytest_asyncio
+from httpx import ASGITransport, AsyncClient
+
+from app.core.config import (
+ DatabaseSettings,
+ Environment,
+ LLMProvider,
+ LLMSettings,
+ ObservabilitySettings,
+ Settings,
+)
+from app.db.session import Database
+from app.domain.traceability import TraceEdge, TraceKind, current_edges
+from app.main import create_app
+
+PREFIX = "/api/v1"
+
+
+@pytest_asyncio.fixture
+async def api() -> AsyncIterator[AsyncClient]:
+ settings = Settings(
+ environment=Environment.TEST,
+ database=DatabaseSettings(
+ url="sqlite+aiosqlite:///file:tracedb?mode=memory&cache=shared&uri=true"
+ ),
+ llm=LLMSettings(provider=LLMProvider.FIXTURE),
+ observability=ObservabilitySettings(log_level="ERROR", json_logs=False),
+ )
+ app = create_app(settings)
+
+ async with (
+ AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client,
+ app.router.lifespan_context(app),
+ ):
+ await app.state.container.resolve(Database).create_schema()
+ yield client
+
+
+async def project_with_work(api: AsyncClient) -> str:
+ """A project advanced past its first approval gate."""
+ project_id: str = (
+ await api.post(
+ f"{PREFIX}/projects",
+ json={
+ "name": "Hospital Management System",
+ "description": "Patients, appointments, billing, doctors.",
+ },
+ )
+ ).json()["id"]
+
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+ pending = (
+ await api.get(f"{PREFIX}/projects/{project_id}/approvals?pending=true")
+ ).json()
+ await api.post(
+ f"{PREFIX}/approvals/{pending[0]['id']}/decision", json={"decision": "approved"}
+ )
+ await api.post(f"{PREFIX}/projects/{project_id}/advance")
+ return project_id
+
+
+# --- Edge deduplication -------------------------------------------------------
+
+
+def test_current_edges_keeps_only_the_latest_declaration() -> None:
+ """An agent that reruns declares a new edge; the old one is history.
+
+ Without this an artifact could never stop being stale: rebuilding it adds a
+ fresh edge, but the superseded one still cites the old upstream version.
+ """
+ older = TraceEdge(
+ project_id="prj_1",
+ upstream_artifact_id="art_up",
+ downstream_artifact_id="art_down",
+ kind=TraceKind.DERIVES_FROM,
+ upstream_version=1,
+ )
+ newer = older.model_copy(
+ update={
+ "id": "edg_new",
+ "upstream_version": 2,
+ "created_at": older.created_at.replace(year=older.created_at.year + 1),
+ }
+ )
+
+ result = current_edges([older, newer])
+
+ assert len(result) == 1
+ assert result[0].upstream_version == 2
+
+
+def test_current_edges_keeps_distinct_dependencies() -> None:
+ """Two different upstreams are two dependencies, not one superseding another."""
+ base = TraceEdge(
+ project_id="prj_1",
+ upstream_artifact_id="art_a",
+ downstream_artifact_id="art_down",
+ upstream_version=1,
+ )
+ other = base.model_copy(update={"id": "edg_2", "upstream_artifact_id": "art_b"})
+
+ assert len(current_edges([base, other])) == 2
+
+
+def test_different_kinds_are_distinct_dependencies() -> None:
+ """`derives_from` and `tests` between the same pair mean different things."""
+ derives = TraceEdge(
+ project_id="prj_1",
+ upstream_artifact_id="art_a",
+ downstream_artifact_id="art_b",
+ kind=TraceKind.DERIVES_FROM,
+ upstream_version=1,
+ )
+ tests = derives.model_copy(update={"id": "edg_2", "kind": TraceKind.TESTS})
+
+ assert len(current_edges([derives, tests])) == 2
+
+
+# --- Graph endpoint -----------------------------------------------------------
+
+
+async def test_graph_returns_nodes_and_edges(api: AsyncClient) -> None:
+ project_id = await project_with_work(api)
+
+ graph = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+
+ assert len(graph["nodes"]) > 0
+ assert len(graph["edges"]) > 0
+ assert graph["stale_artifact_ids"] == []
+
+ node = graph["nodes"][0]
+ assert {"id", "title", "type", "stage", "role", "version", "is_stale"} <= set(node)
+
+
+async def test_graph_edges_reference_only_present_nodes(api: AsyncClient) -> None:
+ """A dangling edge would render as an arrow to nowhere."""
+ project_id = await project_with_work(api)
+
+ graph = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+ ids = {node["id"] for node in graph["nodes"]}
+
+ for edge in graph["edges"]:
+ assert edge["upstream_artifact_id"] in ids
+ assert edge["downstream_artifact_id"] in ids
+
+
+async def test_graph_declares_one_edge_per_dependency(api: AsyncClient) -> None:
+ """Rendering a superseded edge would draw the same dependency twice."""
+ project_id = await project_with_work(api)
+
+ graph = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+ pairs = [
+ (edge["upstream_artifact_id"], edge["downstream_artifact_id"], edge["kind"])
+ for edge in graph["edges"]
+ ]
+
+ assert len(pairs) == len(set(pairs))
+
+
+async def test_graph_marks_staleness_on_nodes_and_edges(api: AsyncClient) -> None:
+ """The UI shows *which* derivation went out of date, not only that one did."""
+ project_id = await project_with_work(api)
+ prd = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()[0]
+
+ await api.post(
+ f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}/revise",
+ json={"body_markdown": "# Revised requirements", "summary": "Scope change"},
+ )
+
+ graph = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+
+ assert graph["stale_artifact_ids"]
+ stale_edges = [edge for edge in graph["edges"] if edge["is_stale"]]
+ assert stale_edges
+ assert stale_edges[0]["current_upstream_version"] > stale_edges[0]["upstream_version"]
+
+
+# --- Impact preview -----------------------------------------------------------
+
+
+async def test_impact_preview_reports_the_blast_radius(api: AsyncClient) -> None:
+ """The question asked *before* the change, not reported after it."""
+ project_id = await project_with_work(api)
+ prd = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()[0]
+
+ preview = (
+ await api.get(f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}/impact")
+ ).json()
+
+ assert preview["artifact_id"] == prd["id"]
+ assert preview["artifact_title"] == prd["title"]
+ assert len(preview["impacted"]) > 0
+ assert preview["stages_affected"]
+
+ item = preview["impacted"][0]
+ assert item["title"], "impacted artifacts are named, not just identified"
+ assert item["depth"] >= 1
+
+
+async def test_impact_preview_changes_nothing(api: AsyncClient) -> None:
+ """Computing impact must never be mistaken for applying it."""
+ project_id = await project_with_work(api)
+ prd = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()[0]
+
+ await api.get(f"{PREFIX}/projects/{project_id}/artifacts/{prd['id']}/impact")
+
+ after = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts?type=prd")).json()[0]
+ graph = (await api.get(f"{PREFIX}/projects/{project_id}/traceability")).json()
+
+ assert after["current_version"] == prd["current_version"]
+ assert graph["stale_artifact_ids"] == []
+
+
+async def test_impact_of_a_leaf_artifact_is_empty(api: AsyncClient) -> None:
+ """A terminal artifact affects nothing downstream."""
+ project_id = await project_with_work(api)
+ artifacts = (await api.get(f"{PREFIX}/projects/{project_id}/artifacts")).json()
+ architecture = next(
+ item for item in artifacts if item["type"] == "system_architecture"
+ )
+
+ preview = (
+ await api.get(
+ f"{PREFIX}/projects/{project_id}/artifacts/{architecture['id']}/impact"
+ )
+ ).json()
+
+ assert preview["impacted"] == []
+ assert preview["stages_affected"] == []
+
+
+async def test_impact_of_an_unknown_artifact_is_a_404(api: AsyncClient) -> None:
+ project_id = await project_with_work(api)
+
+ response = await api.get(
+ f"{PREFIX}/projects/{project_id}/artifacts/art_missing/impact"
+ )
+
+ assert response.status_code == 404
diff --git a/submissions/Victorious/apps/web/.dockerignore b/submissions/Victorious/apps/web/.dockerignore
new file mode 100644
index 00000000..f4c6795e
--- /dev/null
+++ b/submissions/Victorious/apps/web/.dockerignore
@@ -0,0 +1,5 @@
+node_modules/
+.next/
+.env
+.env.local
+npm-debug.log*
diff --git a/submissions/Victorious/apps/web/Dockerfile b/submissions/Victorious/apps/web/Dockerfile
new file mode 100644
index 00000000..767c8c72
--- /dev/null
+++ b/submissions/Victorious/apps/web/Dockerfile
@@ -0,0 +1,53 @@
+# syntax=docker/dockerfile:1
+
+# --- Dependencies ------------------------------------------------------------
+FROM node:22-alpine AS deps
+
+WORKDIR /app
+
+# Manifests only, so the install layer is reused until dependencies change.
+COPY package.json package-lock.json* ./
+RUN npm ci
+
+# --- Builder -----------------------------------------------------------------
+FROM node:22-alpine AS builder
+
+WORKDIR /app
+
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Baked into the client bundle at build time, so it must be supplied as a build
+# arg rather than a runtime environment variable.
+ARG NEXT_PUBLIC_API_URL=http://localhost:8000
+ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN npm run build
+
+# --- Runtime -----------------------------------------------------------------
+FROM node:22-alpine AS runtime
+
+WORKDIR /app
+
+ENV NODE_ENV=production \
+ NEXT_TELEMETRY_DISABLED=1 \
+ PORT=3000
+
+RUN addgroup --system --gid 1001 nodejs \
+ && adduser --system --uid 1001 nextjs
+
+# `output: "standalone"` produces a self-contained server: no node_modules in the
+# final image, which cuts it from roughly 1.2 GB to under 200 MB.
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+COPY --from=builder --chown=nextjs:nodejs /app/public ./public
+
+USER nextjs
+
+EXPOSE 3000
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
+ CMD node -e "require('http').get('http://127.0.0.1:3000',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
+
+CMD ["node", "server.js"]
diff --git a/submissions/Victorious/apps/web/app/dashboard/loading.tsx b/submissions/Victorious/apps/web/app/dashboard/loading.tsx
new file mode 100644
index 00000000..97217bf5
--- /dev/null
+++ b/submissions/Victorious/apps/web/app/dashboard/loading.tsx
@@ -0,0 +1,44 @@
+import { Skeleton, SkeletonCard, SkeletonRegion } from "@/components/ui/skeleton";
+
+/**
+ * Dashboard loading state.
+ *
+ * Mirrors the real layout — three stat tiles, then a project list — so the page
+ * does not reflow when data lands. The alternative, a centred spinner, tells the
+ * user nothing about what is coming and guarantees a jump when it does.
+ */
+export default function DashboardLoading() {
+ return (
+
+ {project.name}
+
+ {project.description}
+
+ The workspace hit an unexpected error. Your project data is unaffected —
+ engineering artifacts are only written through the API.
+
+ Reference: {error.digest}
+
+ .venv/Scripts/python -m uvicorn app.main:app --reload
+ {" "}
+ — the interpreter has to be the project's virtualenv, or a bare{" "}
+ uvicorn resolves to a global
+ install that has none of the dependencies and exits without ever binding
+ the port. Then reload this page.
+ >
+ }
+ />
+
+ {approvals.map((approval) => (
+
+
+ {projects.map((project) => (
+
+ )}
+
+ Something went wrong
+
+
+ This page does not exist. If you followed a link to a project, it may have + been created against a database that has since been reset. +
++ Not another coding assistant. Specialized AI engineering agents coordinate + requirements, architecture, implementation, testing, and documentation over a + shared organizational memory — with full traceability and human approval at + every gate. +
+ +{pillar.body}
++ AI has made writing code fast. It has not made coordinating the + decisions around it fast. Existing tools optimise one stage each, and + nothing continuously answers: +
+ ++ Project Victorious occupies that layer. It coordinates the engineering + lifecycle rather than accelerating one step of it. +
++ Requested by the Executive AI · blocks {stageLabel(approval.stage)} +
++ + Downstream impact + {" "} + · {approval.impacted.length} artifact + {approval.impacted.length === 1 ? "" : "s"} depend on this +
+{approval.title}
+ {approval.feedback && ( ++ “{approval.feedback}” +
+ )} ++ {Icon && } + {children} +
+ ); +} + +function Field({ + label, + icon, + children, +}: { + label: string; + icon?: React.ComponentType<{ className?: string }> | undefined; + children: React.ReactNode; +}) { + return ( +{children}
++ Out of date with its upstream +
++ This was derived from an earlier version of something that has since + changed.{" "} + + See what it depends on → + +
++ {project.description} +
+ ++ Overall project +
++ {summary.overall_score >= 85 + ? "Strong across the organization" + : summary.overall_score >= 70 + ? "Sound, with room to tighten" + : "Needs attention before shipping"} +
++ Mean of every artifact review. The tick on the ring marks 85 — the + threshold for a strong review. +
+{role.role_title}
++ {`${role.artifacts_reviewed} artifact${role.artifacts_reviewed === 1 ? "" : "s"} · lowest ${role.lowest_score}`} +
+ {role.needs_revision > 0 && ( +
+ 07_System_Architecture.md
+ {" "}
+ requires.
+
+ >
+ }
+ className="animate-[rise_0.4s_var(--ease-out-quint)_both]"
+ />
+ );
+}
+
+function Stat({
+ label,
+ value,
+ suffix,
+ alarming = false,
+}: {
+ label: string;
+ value: number;
+ suffix?: string;
+ alarming?: boolean;
+}) {
+ return (
+ + {typeLabel(node.type)} · {stageLabel(node.stage)} · v + {node.version} +
++ Derived from +
++ {failed && ( + + )} + {outcome} +
+ )} +{stageLabel(agent.stage)}
+{agent.task}
+ ) : ( ++ Waiting for upstream work to reach this stage. +
+ )} + + {agent.blocked_on.length > 0 && ( ++ + Blocked on {agent.blocked_on.length} dependency + {agent.blocked_on.length === 1 ? "" : "s"} +
+ )} + + {agent.confidence !== null && ( ++ {agent.reasoning_summary} +
++ {agent.provider} · {agent.model} +
+ ) : ( + + )} + + {agent.output_artifact_ids.length > 0 && ( + + {agent.output_artifact_ids.length} artifact + {agent.output_artifact_ids.length === 1 ? "" : "s"} + + + )} ++ + {error} +
+ )} + +{children}
, + ul: ({ children }) => ( ++ {children} ++ ), + hr: () => ( +
+ {children}
+
+ );
+ }
+
+ return (
+
+
+ {source}
+
+
+
+ {artifact.title}
+ {artifact.is_stale && (
+
+ {typeLabel(artifact.type)} · {artifact.owner_title} · v + {artifact.current_version} +
++ A name and a description is all the organization needs. It works out the + requirements from there. +
+
+ {chart}
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/submissions/Victorious/apps/web/components/review/review-card.tsx b/submissions/Victorious/apps/web/components/review/review-card.tsx
new file mode 100644
index 00000000..834783da
--- /dev/null
+++ b/submissions/Victorious/apps/web/components/review/review-card.tsx
@@ -0,0 +1,247 @@
+import Link from "next/link";
+import { CircleCheck, CircleAlert, Lightbulb, TriangleAlert } from "lucide-react";
+
+import { ScoreRing } from "@/components/review/score-ring";
+import { StatusBadge } from "@/components/ui/status-badge";
+import { Card } from "@/components/ui/card";
+import {
+ scoreState,
+ stageLabel,
+ type ReviewFindingView,
+ type ReviewView,
+} from "@/lib/api";
+import { cn } from "@/lib/utils";
+
+const VERDICT_LABEL: Record+ {review.role_title} · {stageLabel(review.stage)} · v + {review.artifact_version} +
+{review.summary}
+ ++ checks {deterministic}/100 + {reasoningApplied ? ( + <> + {" · "}reasoning {delta > 0 ? `+${delta}` : delta} + {model ? ` (${model})` : ""} + > + ) : ( + " · checks only" + )} +
++ {title} +
++ v{review.artifact_version} · checks {review.deterministic_score}/100 +
+{review.summary}
+ + } + /> + } + /> ++ + Not reviewed. Reviews are written when an agent produces an artifact; a human + revision is not reviewed until the artifact is regenerated. +
+ ); +} diff --git a/submissions/Victorious/apps/web/components/review/score-ring.tsx b/submissions/Victorious/apps/web/components/review/score-ring.tsx new file mode 100644 index 00000000..751175d1 --- /dev/null +++ b/submissions/Victorious/apps/web/components/review/score-ring.tsx @@ -0,0 +1,129 @@ +import { cn } from "@/lib/utils"; + +/** + * A quality score as a ring. + * + * One number carries the whole Helix Review view, so it gets a shape rather than + * a bare figure — an arc is readable at a glance from across a room, which a + * two-digit number is not. + * + * Three details make it read as *measured* rather than styled: + * + * - The arc draws itself in from zero, so the score arrives the way a + * measurement does. `prefers-reduced-motion` renders it at its final value. + * - A faint tick marks the 85 threshold where a review counts as strong, so a + * score is legible against the bar it is judged by, not just in isolation. + * - Colour follows the same score bands the badges use, and the numeral is + * always rendered — colour alone must never be the only carrier of meaning + * (`10_UI_UX_Plan.md`, Accessibility). + */ + +const STRONG_THRESHOLD = 85; + +export function ScoreRing({ + score, + size = 96, + label, + showThreshold = true, +}: { + score: number; + size?: number; + label?: string; + showThreshold?: boolean; +}) { + const clamped = Math.max(0, Math.min(100, score)); + const stroke = size >= 80 ? 6 : size >= 60 ? 5 : 4; + const radius = (size - stroke) / 2; + const circumference = 2 * Math.PI * radius; + const filled = (clamped / 100) * circumference; + + const tone = + clamped >= 85 + ? "stroke-state-complete" + : clamped >= 70 + ? "stroke-state-active" + : clamped >= 60 + ? "stroke-state-waiting" + : "stroke-state-blocked"; + + const glow = + clamped >= 85 + ? "var(--color-state-complete)" + : clamped >= 70 + ? "var(--color-state-active)" + : clamped >= 60 + ? "var(--color-state-waiting)" + : "var(--color-state-blocked)"; + + // The 85 tick, positioned on the same -90° rotated axis as the arc. + const thresholdAngle = (STRONG_THRESHOLD / 100) * 2 * Math.PI - Math.PI / 2; + const centre = size / 2; + + return ( ++ This appends a new version. Everything derived from the current version will + be flagged as out of date, and the organization will offer to rebuild it. +
++ + + + {impact.impacted.length} artifacts + {" "} + depend on this and would go out of date + {impact.stages_affected.length > 0 && ( + <> · {impact.stages_affected.length} stages would rerun> + )} + +
++ {error} +
+ )} + +
+ The workspace could not reach the Victorious API. From{" "}
+ apps/api, start it
+ with{" "}
+
+ .venv/Scripts/python -m uvicorn app.main:app --reload
+ {" "}
+ — a bare uvicorn resolves to a
+ global install without the dependencies, which exits without binding the
+ port.
+
+ Overall platform status is {health.status} with {health.components.length}{" "} + components reporting. +
++ Nothing has happened yet. Use{" "} + Advance engineering{" "} + to start the organization. +
+{event.summary}
++ {new Date(event.created_at).toLocaleTimeString()} + {event.role && ` · ${event.role.replace(/_/g, " ")}`} +
+{stageLabel(stage.stage)}
++ {stage.owner_title ?? "Unassigned"} + {stage.artifact_count > 0 && + ` · ${stage.artifact_count} artifact${stage.artifact_count === 1 ? "" : "s"}`} +
+{title}
++ {description} +
+ + {action &&{hint}
} ++ {eyebrow} +
+ )} ++ {description} +
+ )} +
+
{label}
+ {hint &&{hint}
} +