# Project export: Foreman

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Contractor invoicing takes hours of manual work. Foreman uses three AI agents to draft, verify, and approve invoices, with a human in the loop at every step that matters.
- Devpost: https://devpost.com/software/foreman-j2kscg
- GitHub: https://www.github.com/eugenelacatis/foreman
- Video: https://www.youtube.com/embed/iJYwt89fWMI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Eugene Lacatis (44 commits), harshita (22 commits), Claude Opus 4.7 (1M context) (11 commits), cookiebytegirlie (11 commits), BhoomikaGuptaa (9 commits)

## Devpost submission (written by the team)

### Inspiration

A plumber, an HVAC tech, or an electrician spends 30 to 60 minutes after every job writing up an invoice: pulling rates from memory, tracking down what parts were used, formatting line items, emailing the vendor. It's the least skilled part of their day and the most likely to have errors that delay payment. We wanted to see how much of that could disappear if an AI agent had already read the work order, prefilled everything it could, and only asked the human for the two or three things it genuinely couldn't figure out on its own. What we built Foreman is a multi-agent pipeline that moves a field service work order from raw request to approved invoice. Intake reads the unstructured request, classifies the job type, pulls out the relevant entities (location, vendor, urgency), and flags anything missing before the order goes further. Scheduling proposes appointment windows, drafts customer outreach, and suggests parts likely needed for the job, clearly labeled as estimates rather than confirmed quotes. Invoicing is the deep stage. It prefills an invoice from everything already known, identifies the specific gaps (labor rate, hours, trip charge), has a natural-language conversation with the user to fill only those gaps, checks the draft against past invoices for rate consistency, renders a branded invoice, and drafts the vendor notification email. The human is in the loop at every commit point. Agents propose; a person confirms. No stage advances without explicit approval. The demo highlight is an ArmorIQ safety check blocking an off-plan action mid-invoice: the agent tries to commit, the gate fires, and the operator sees exactly why. The shared state is a single work-order object in Redis. Agents don't call each other; each one reads what came before it and writes its own section. That let four people build in parallel on day one without stepping on each other.

### How we built it

Agents run on Anthropic Claude via the SDK, using tool use directly with no framework wrapper. Each agent has a focused set of tools, a system prompt that describes the turn-based flow, and a fallback to seeded data if an external call fails. Redis holds the work-order object and invoice history. Every agent reads from and writes to it; the pipeline advances when an approval gate opens. ArmorIQ wraps the committing actions (filling the template, drafting the vendor email). Every action is signed with a plan; off-plan actions are blocked at runtime and surfaced to the operator. Arize Phoenix instruments every Claude call. Each agent decision, including gap-fill questions asked, consistency flags raised, and ArmorIQ checks, appears as a span in the Phoenix UI and links back to the work order through a trace ID. The API is FastAPI with a locked OpenAPI contract that all four team members built against from the start. The frontend is Vite, React, Tailwind, and shadcn/ui. Challenges Conversation state across HTTP requests. The invoicing agent needs to pick up mid-conversation when the user responds. Naively, every POST to /invoice-chat restarted the agent from scratch and re-asked the same questions. We fixed this by persisting the full Claude message history in the Invoice object in Redis, so turn two resumes exactly where turn one left off. A broken Phoenix dependency. arize-phoenix 6.2.0 ships with a broken internal import when arize-phoenix-evals is installed separately. We had to pin compatible versions and wrap all Phoenix imports in graceful fallbacks so a missing tracing dependency never crashes the agent. Four people, one schema. Locking the work-order schema in the first 90 minutes was the right call. Every argument about field names happened before anyone wrote code, which meant no merge conflicts on the object everyone reads and writes.

### What we learned

The human approval gate is not a feature you add to an agentic system. It's the architecture. Designing it as a real stop, not a cosmetic checkbox, forced every other decision: how state is held, how agents are prompted, how ArmorIQ fits in. Getting that right early made the rest of the build feel coherent. Conversation history is also load-bearing in a way we didn't fully appreciate at the start. An agent that asks the same question twice isn't just annoying; it breaks the user's trust that the system understood them. Persisting and resuming message history is the difference between something that feels like a product and something that feels like a prototype.

## README (from the GitHub repository)

# Foreman

Multi-agent field service invoicing. A work order flows through three AI agents (intake, scheduling, invoicing) with a human approving every step that actually commits anything.

## How it works

```
Raw request → Intake → Scheduling → Invoicing → Approved invoice + vendor email
```

Each agent reads the shared work-order object in Redis, writes its own section, and stops at a human approval gate before the next stage runs. Agents don't call each other; they pass state through the object.

The invoicing stage is a multi-turn conversation. The agent prefills what it can from the work order, asks only for what's missing (labor rate, hours, trip charge), checks the draft against past invoices for rate consistency, then produces a branded invoice and vendor email draft. Nothing commits without human sign-off.

## Quick start

You need Python 3.11, Redis Stack (not plain Redis), and an Anthropic API key. Redis Stack includes the vector search module required for invoice history. Install it with `brew tap redis-stack/redis-stack && brew trust redis-stack/redis-stack && brew install redis-stack`, then start it with `redis-stack-server --port 6380 --daemonize yes`.

```bash
cp backend/.env.example backend/.env
# fill in ANTHROPIC_API_KEY in backend/.env

make dev    # starts Redis + Phoenix + uvicorn on :8001
make test   # runs the smoke test suite (no API key needed)
```

## API

| Endpoint | What it does |
|---|---|
| `POST /work-orders` | Create a work order from a raw request |
| `GET /work-orders/{id}` | Fetch current state |
| `POST /work-orders/{id}/approve` | Approve a stage (triggers the next agent) |
| `POST /work-orders/{id}/invoice-chat` | Send a message to the invoicing agent |
| `GET /work-orders/{id}/invoice-history` | Pull past invoices for the consistency check |

## Integrations

- **Anthropic** — all three agents use Claude with tool use
- **Arize Phoenix** — every Claude call is traced; spans link back to the work order via `trace_id`, visible at `http://localhost:6006`
- **ArmorIQ** — committing actions are signed and checked at runtime; `DEMO_BLOCK` triggers a visible operator alert
- **Redis** — work-order state and invoice history

## Environment variables

```
ANTHROPIC_API_KEY=
REDIS_URL=redis://localhost:6379
PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006/v1/traces
ARMORIQ_API_KEY=          # optional; stub works without it for demo
ARIZE_SPACE_ID=           # optional; local Phoenix works without it
ARIZE_API_KEY=            # optional; local Phoenix works without it
```

## Team

Eugene (invoicing agent + API spine), Bhoomika (orchestration + Redis), Harshita (intake + scheduling), Michelle (UI + demo)


## Detected evidence (automated analysis)

Indexed codebase: 90 recognized source files, 523 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Redis (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (103 of 103)

```
.gitignore
backend/.env.example
backend/agents/armoriq_client.py
backend/agents/browserbase_client.py
backend/agents/intake_agent.py
backend/agents/invoice_template.py
backend/agents/invoicing_agent.py
backend/agents/parts_client.py
backend/agents/question_map.py
backend/agents/scheduling_agent.py
backend/agents/voice_client.py
backend/api/__init__.py
backend/api/orkes_routes.py
backend/api/routes.py
backend/main.py
backend/models/__init__.py
backend/models/work_order.py
backend/orchestration/__init__.py
backend/orchestration/pipeline.py
backend/orkes/__init__.py
backend/orkes/agentspan_foreman.py
backend/pytest.ini
backend/requirements.txt
backend/scripts/__init__.py
backend/scripts/gen_test_audio.py
backend/scripts/smoke_front.py
backend/scripts/test_armoriq.py
backend/scripts/test_consistency.py
backend/scripts/test_fallback.py
backend/scripts/test_gap_fill.py
backend/scripts/test_scenarios.py
backend/scripts/test_voice_turn.py
backend/scripts/test_voice.py
backend/seeds/__init__.py
backend/seeds/invoice_history.py
backend/state/__init__.py
backend/state/invoice_history.py
backend/state/redis_client.py
backend/state/seed.py
backend/tests/__init__.py
backend/tests/conftest.py
backend/tests/test_intake_agent.py
backend/tests/test_scheduling_agent.py
backend/tests/test_voice_client.py
CLAUDE.bhoomika.md
CLAUDE.eugene.md
CLAUDE.harshita.md
CLAUDE.md
CLAUDE.michelle.md
DESIGN-FLOW-AUDIT.md
FLOW-COVERAGE.md
frontend/.env.example
frontend/CLAUDE.md
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/public/fonts/Figtree/OFL.txt
frontend/public/fonts/Figtree/README.txt
frontend/public/fonts/Quicksand/OFL.txt
frontend/public/fonts/Quicksand/README.txt
frontend/src/api/client.ts
frontend/src/App.tsx
frontend/src/components/ApprovalsView.tsx
frontend/src/components/ArmorIQBlock.tsx
frontend/src/components/ClientDetailView.tsx
frontend/src/components/Clients.tsx
frontend/src/components/ClientsView.tsx
frontend/src/components/DropZone.tsx
frontend/src/components/IntakeView.tsx
frontend/src/components/invoice-flow/ArmorIQGate.tsx
frontend/src/components/invoice-flow/BrandedInvoice.tsx
frontend/src/components/invoice-flow/InboundPartsView.tsx
frontend/src/components/invoice-flow/PricingView.tsx
frontend/src/components/invoice-flow/ScheduleStep.tsx
frontend/src/components/invoice-flow/WorkOrderToInvoiceFlow.tsx
frontend/src/components/InvoicingView.tsx
frontend/src/components/NeedsYou.tsx
frontend/src/components/SchedulingView.tsx
frontend/src/components/SearchBar.tsx
frontend/src/components/SectionHeading.tsx
frontend/src/components/Sidebar.tsx
frontend/src/components/VoiceIntake.tsx
frontend/src/components/WorkOrderPipeline.tsx
frontend/src/components/WorkOrders.tsx
frontend/src/index.css
frontend/src/lib/sentry.ts
frontend/src/main.tsx
frontend/src/vite-env.d.ts
frontend/tailwind.config.js
frontend/tsconfig.json
frontend/vite.config.ts
Makefile
README.md
run_all_tests.sh
scripts/demo_flow.py
scripts/DEMO_SCRIPT.md
scripts/start.sh
scripts/test_agentspan_orkes.py
scripts/test_gate_blocks.py
scripts/test_redis_roundtrip.py
slides/index.html
slides/STORY.md
test_workorder.txt
```

### Dependencies

- backend/requirements.txt: anthropic@==0.54.0, arize-phoenix@==4.29.0, arize-phoenix-evals@==0.29.0, arize-phoenix-otel@==0.16.1, browserbase@==1.4.0, fastapi@==0.115.5, httpx@==0.28.0, openinference-instrumentation-anthropic@==0.1.17, playwright@==1.49.0, pydantic@==2.10.3, pytest@==8.3.4, pytest-asyncio@==0.24.0, python-dotenv@==1.0.1, redis[hiredis]@==5.2.1, sentence-transformers@==3.3.1, sentry-sdk[fastapi]@==2.22.0, uvicorn[standard]@==0.32.1, websockets@>=13.1
- frontend/package.json: @radix-ui/react-dialog@^1.1.2, @radix-ui/react-slot@^1.1.0, @react-leaflet/core@^1.0.2, @sentry/react@^10.59.0, @types/leaflet@^1.9.21, @types/react@^18.3.12, @types/react-dom@^18.3.1, @vitejs/plugin-react@^4.3.3, autoprefixer@^10.4.20, class-variance-authority@^0.7.0, clsx@^2.1.1, leaflet@^1.9.4, lucide-react@^1.21.0, postcss@^8.4.49, react@^18.3.1, react-dom@^18.3.1, react-leaflet@^3.2.5, tailwind-merge@^2.5.4, tailwindcss@^3.4.15, typescript@^5.6.3, vite@^5.4.11

### Recent commits (newest first)

- Update .env.example to remove API keys
- final project
- Merge pull request #24 from eugenelacatis/michelle/frontend-integration
- frontend: Clients, Approvals, ClientDetail views + sidebar nav wiring
- Merge pull request #23 from eugenelacatis/eugene
- integrate invoice and map fixes
- Merge pull request #22 from eugenelacatis/michelle/frontend-integration
- Merge pull request #21 from eugenelacatis/harshita
- Merge branch 'main' into harshita
- voice agent only
- Merge pull request #20 from eugenelacatis/bhoomika-final-fixes
- Finalize approval flow  and Leaflet supplier map
- feat: add ScheduleStep, ArmorIQGate, wire invoice flow
- Merge pull request #19 from eugenelacatis/harshita
- updated the code for voice input-frontend
- Merge pull request #18 from eugenelacatis/fix/dashboard-jsx-unclosed-div
- fix: close dashboard wrapper div, remove duplicate intake block
- Merge pull request #17 from eugenelacatis/michelle/intake-clarity
- Merge branch 'main' into michelle/intake-clarity
- frontend: wire all dead entry points + add resilient error states throughout

## Key source files (fetched from GitHub, selected and truncated for size)

### CLAUDE.michelle.md

```markdown
# CLAUDE.michelle.md (Designer: interface and demo)

Read `CLAUDE.md` (root) first. This file fences your role. You own the entire interface and the demo video. This is the most important visible job on the team, because the UI and the demo are all the judges actually see and score.

## You own

### 1. The interface (Vite + React + Tailwind + shadcn/ui)
A dashboard that shows a work order moving through its three stages, with the agents visibly working and the human approving.

The screens that matter:
- The work order arriving and intake classifying it.
- Scheduling proposing times, the outreach draft, and the labeled parts suggestion.
- Invoicing: the side-by-side of work order to filled invoice, the gap-fill conversation, the consistency check, and the branded invoice draft.
- The human-approval moments. These should feel like deliberate, satisfying confirmations, because the approval gate is the whole thesis.
- The ArmorIQ block firing when an agent tries an off-plan action. This is a demo highlight. Make it legible and a little dramatic.
- The Arize traces, surfaced enough that a judge can see the agent's reasoning is real.

### 2. The demo video
The recorded fallback in case anything breaks live. Own the two-minute cut: open on the manual invoice pain, then ForemanAI doing it cleanly, ending on the approval gate and the time saved.

## You must NOT
- Build agent logic or backend. You consume the API the spine exposes.
- Wait for the backend to be finished before starting. Build against the OpenAPI contract and mock data from day one.

## Guardrails for your Claude Code session
- Use shadcn/ui components so you spend time on the demo flow and the polish, not on building primitives.
- Build against the OpenAPI spec and seeded data immediately. Do not block on the real agents being ready. Swap mock data for live calls late.
- The two lean stages (intake, scheduling) must look as finished as invoicing. Coordinate with Person A so the front of the lifecycle does not look like a placeholder next to the deep invoicing screen.
- This file sets up what to build. The actual interface design and interaction polish are yours. Make it feel like a real product, not a hackathon shell.

## Done looks like
A judge can watch a work order flow through all three stages, see the agents reason, approve at each gate, watch the ArmorIQ block fire, and read the time saved, all in a polished UI, with a recorded video ready if the live demo stumbles.

## Task list

### Foundation (do first)
- [x] Vite + React + Tailwind + TypeScript project scaffold (`frontend/`)
- [x] Typed API client for all backend endpoints (`frontend/src/api/client.ts`)
- [x] Work order submit form + top-level app state (`frontend/src/App.tsx`)
- [ ] Run `npm install` and confirm dev server starts (`npm run dev`)
- [ ] Install shadcn/ui: `npx shadcn@latest init` and add `card`, `badge`, `button`, `textarea` components
- [ ] Confirm API proxy to `http://localhost:8000` works (submit a test work o
[truncated — 1661 more characters]
```

### CLAUDE.eugene.md

```markdown
# CLAUDE.eugene.md

Read `CLAUDE.md` (root) first. This file fences your role. You own two things: the invoicing agent (the deep one) and the integration spine.

## You own

### 1. The invoicing agent
The deepest agent in the system. It reads the work-order object after scheduling has enriched it, and produces a complete, verified vendor invoice plus a draft email.

What it has to do:
- Prefill an invoice from the work-order data already in the object, so it starts mostly complete, not blank.
- Identify what is still missing (rates, trip charge, quantities, whatever the template needs that the work order does not contain).
- Run a short back-and-forth with the user, in natural language, to fill only the gaps. Not a form, a conversation that ends when the invoice is complete.
- Check the draft against past invoices for that vendor (read the invoice-history search the spine exposes) for consistency in formatting, line items, and rate sanity. Flag anything off.
- Fill OUR branded invoice template. Not a copy of the user's personal style. Professional by default.
- Present the draft for human verification. Nothing commits without it.
- On approval, draft the vendor email to the vendor who requested the work. Stop at draft. No send.

You write only the `invoice` section of the work-order object. You read `raw_request`, `classification`, and `schedule`.

### 2. The integration spine (shared with Person B)
- Lock the work-order schema in the first 90 minutes with Person B and the team. This is your highest priority before any agent code.
- Own the FastAPI app and the OpenAPI contract everyone builds against.
- Coordinate the async orchestration that advances the work order through stages.
- Stage two: lead final integration alongside Person B.

## You must NOT
- Build the intake or scheduling agents. That is Person A.
- Build the UI. That is the Designer.
- Auto-approve anything to smooth the demo. The human gate is the thesis.
- Build procurement, live send, or payments.

## Guardrails for your Claude Code session
- The invoicing conversation logic and consistency-checking are the hard, interesting part. Reason through them yourself. This file does not hand you the solution.
- Wrap your agent's committing actions with ArmorIQ so the off-plan block can be demoed on the invoicing step.
- Emit Arize traces for each agent decision.
- Build the gap-fill and consistency check to degrade to seeded invoice history if the search is not ready yet, so you are never blocked waiting on the spine.

## Done looks like
A seeded work order flows in, the agent prefills and runs a real gap-fill conversation, checks consistency, presents a branded draft, waits for human approval, and produces a vendor email draft. ArmorIQ blocks a deliberately off-plan action. Every step traces in Arize.

## Task list

### Spine (do first, blocks everyone)
- [x] Lock work-order schema with Bhoomika (`backend/models/work_order.py`)
- [x] Stand up FastAPI app with CORS and `/health` (`backe
[truncated — 1768 more characters]
```

### backend/requirements.txt

```
fastapi==0.115.5
uvicorn[standard]==0.32.1
pydantic==2.10.3
redis[hiredis]==5.2.1
anthropic==0.54.0
arize-phoenix==4.29.0
arize-phoenix-evals==0.29.0
arize-phoenix-otel==0.16.1
openinference-instrumentation-anthropic==0.1.17
httpx==0.28.0
websockets>=13.1
browserbase==1.4.0
playwright==1.49.0
python-dotenv==1.0.1
sentry-sdk[fastapi]==2.22.0
sentence-transformers==3.3.1
pytest==8.3.4
pytest-asyncio==0.24.0

```

### frontend/package.json

```
{
  "name": "foremanai-frontend",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-slot": "^1.1.0",
    "@react-leaflet/core": "^1.0.2",
    "@sentry/react": "^10.59.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "leaflet": "^1.9.4",
    "lucide-react": "^1.21.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-leaflet": "^3.2.5",
    "tailwind-merge": "^2.5.4"
  },
  "devDependencies": {
    "@types/leaflet": "^1.9.21",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.3",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.49",
    "tailwindcss": "^3.4.15",
    "typescript": "^5.6.3",
    "vite": "^5.4.11"
  }
}

```

### backend/main.py

```python
from __future__ import annotations

import os
from contextlib import asynccontextmanager
from typing import AsyncGenerator

import sentry_sdk
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration

from backend.api.orkes_routes import orkes_router
from backend.api.routes import router
from backend.orkes.agentspan_foreman import shutdown as agentspan_shutdown
from backend.state.redis_client import close_redis, init_redis, _client
from backend.state.seed import seed_demo_work_orders

sentry_sdk.init(
    dsn=os.getenv("SENTRY_DSN"),
    integrations=[StarletteIntegration(), FastApiIntegration()],
    auto_enabling_integrations=False,
    traces_sample_rate=0.0,
    environment=os.getenv("ENV", "development"),
)


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    await init_redis()
    await _client().ping()
    await seed_demo_work_orders()
    yield
    await agentspan_shutdown()
    await close_redis()


app = FastAPI(title="ForemanAI", version="0.1.0", lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(router)
app.include_router(orkes_router)


@app.get("/health")
async def health() -> dict:
    return {"status": "ok"}

```

### frontend/src/main.tsx

```typescript
import * as Sentry from "@sentry/react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  tracesSampleRate: 0,
});

ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

```

### frontend/src/App.tsx

```typescript
import { useState } from "react";
import { Mic } from "lucide-react";
import Sidebar from "./components/Sidebar";
import DropZone from "./components/DropZone";
import SearchBar from "./components/SearchBar";
import type { SearchResult } from "./components/SearchBar";
import NeedsYou from "./components/NeedsYou";
import WorkOrders from "./components/WorkOrders";
import WorkOrderToInvoiceFlow from "./components/invoice-flow/WorkOrderToInvoiceFlow";
import type { StepKey } from "./components/invoice-flow/WorkOrderToInvoiceFlow";
import VoiceIntake from "./components/VoiceIntake";
import ClientsView from "./components/ClientsView";
import ApprovalsView from "./components/ApprovalsView";
import { createWorkOrder } from "./api/client";
import type { WorkOrder } from "./api/client";

type View = "dashboard" | "invoice-flow" | "clients" | "approvals";

export default function App() {
  const [view, setView] = useState<View>("dashboard");
  const [workOrderId, setWorkOrderId] = useState<string | null>(null);
  const [fileName, setFileName] = useState<string | null>(null);
  const [showVoice, setShowVoice] = useState(false);
  const [intakeLoading, setIntakeLoading] = useState(false);
  const [backendError, setBackendError] = useState<string | null>(null);
  const [initialStep, setInitialStep] = useState<StepKey>("inbound");
  const [pendingApprovals, setPendingApprovals] = useState(3);

  const openFlow = (
    id: string | null,
    step: StepKey,
    title?: string | null,
  ) => {
    setWorkOrderId(id);
    setFileName(title ?? null);
    setInitialStep(step);
    setIntakeLoading(false);
    setBackendError(null);
    setView("invoice-flow");
  };

  const startFlowWithFile = async (file: File) => {
    setIntakeLoading(true);
    let rawRequest: string;
    if (file.name.endsWith(".txt") || file.name.endsWith(".eml")) {
      rawRequest = await file.text().catch(() => file.name);
    } else {
      rawRequest = file.name;
    }
    try {
      const wo = await createWorkOrder(rawRequest);
      openFlow(wo.id, "inbound", file.name);
    } catch {
      setIntakeLoading(false);
      setWorkOrderId(null);
    }
  };

  const startFlowWithText = async (text: string) => {
    setIntakeLoading(true);
    try {
      const wo = await createWorkOrder(text);
      openFlow(wo.id, "inbound", null);
    } catch {
      setIntakeLoading(false);
      setWorkOrderId(null);
    }
  };

  const startFlowWithVoice = (wo: WorkOrder) => {
    setShowVoice(false);
    openFlow(wo.id, "inbound", "voice-intake.wav");
  };

  const backToDashboard = () => {
    setView("dashboard");
    setWorkOrderId(null);
    setFileName(null);
    setBackendError(null);
  };

  return (
    <div className="flex min-h-screen items-start bg-white text-[var(--color-ink)]">
      <Sidebar
        activeKey={view === "invoice-flow" ? "dashboard" : view}
        onNav={(key) => {
          if (key === "clients") setView("clients");
          else if (key === "approvals") setView("approvals");
          else if (key === "dashboard") backToDashboard();
        }}
        approvalsCount={pendingApprovals}
      />

      <main className="flex-1 min-w-0">
        <div className="mx-auto w-full max-w-[1100px] px-5 sm:px-8 lg:px-12 py-8 lg:py-10">
          {view === "approvals" ? (
            <ApprovalsView
              onBack={backToDashboard}
              onSent={() => setPendingApprovals((p) => Math.max(0, p - 1))}
            />
          ) : view === "clients" ? (
            <ClientsView />
          ) : view === "dashboard" ? (
            <>
              <p className="mb-6 text-[11.5px] font-semibold uppercase tracking-widest text-[var(--color-ink-3)]">
                Dashboard
              </p>

              <div className="flex flex-col gap-8">
                <NeedsYou
                  onApprove={(id) => openFlow(id, "invoice")}
                  onView={(id) => openFlow(id, "inbound")}
                />

                <div className="h-px bg-[var(--color-hairline)]" />

                <section>
                  <p className="mb-3 text-[11.5px] font-semibold uppercase tracking-widest text-[var(--color-ink-3)]">
                    New work order
                  </p>
                  <DropZone
                    onFile={startFlowWithFile}
                    onText={startFlowWithText}
                    loading={intakeLoading}
                  />
                  <div className="mt-3 flex items-center gap-3">
                    <div className="h-px flex-1 bg-[var(--color-hairline)]" />
                    <span className="text-[12.5px] text-[var(--color-ink-3)]">
                      or
                    </span>
                    <div className="h-px flex-1 bg-[var(--color-hairline)]" />
                  </div>
                  <button
                    type="button"
                    onClick={() => setShowVoice(true)}
                    className="mt-3 flex w-full items-center justify-center gap-2.5 rounded-[10px] border border-[var(--color-hairline)] bg-white py-4 text-[14px] font-medium text-[var(--color-ink)] transition-colors hover:border-[var(--color-accent)] hover:text-[var(--color-accent)] hover:bg-[var(--color-accent-tint)]"
                  >
                    <span className="grid h-8 w-8 place-items-center rounded-full bg-[var(--color-accent-tint)] text-[var(--color-accent)]">
                      <Mic size={16} strokeWidth={2} />
                    </span>
                    Describe the work order by voice
                  </button>
                </section>

                <section>
                  <SearchBar
                    onSelect={(r: SearchResult) => {
                      if (r.type === "workOrder")
                        openFlow(r.id, "inbound", r.title);
                    }}
                  />
                  <div className="mt-4">
                    <WorkOrders
                      onViewOrder={(row) =>
                        openFlow(row.id
[truncated — 634 more characters]
```

### run_all_tests.sh

```shell
#!/usr/bin/env bash
set -e

echo "--- consistency check ---"
python3.11 -m backend.scripts.test_consistency

echo "--- gap-fill loop ---"
python3.11 -m backend.scripts.test_gap_fill

echo "--- armoriq block ---"
python3.11 -m backend.scripts.test_armoriq

echo "--- front fallback ---"
python3.11 -m backend.scripts.test_fallback

echo ""
echo "All tests passed."

```

### frontend/postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ForemanAI</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

[73 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]