Phase 1 (pipeline blockers):
- Stage 6 normalizer: move Claude call to BackgroundTasks (fixes 504 at proxy);
frontend polls useStageArtifacts(6) + useClientAssets every 2s with a
2-min soft cap and a "Normalizing… ~30s" banner.
- Stage 8 ratecard: raise ValueError when no Stage-7 matches selected so
the user gets a clear 400 instead of silent "0 lines built" success.
- Stage 4 Q&A pack: visible amber empty-state callout when no clarifications.
- Stage 1 intake: green CTA banner after metadata lands telling the user
to scroll down and click Complete Stage 1.
- APP_PUBLIC_URL: log a warning at startup if empty / not fully-qualified
so approval-email links don't ship as broken relative URLs.
Phase 2 (reviewer UX):
- Remove "Operating model" select from intake form (sales lead doesn't
know the solution yet); default model_type='current_oplus'.
- Inline-edit pencil for opportunity name in OpportunityView header.
- Stage 3 TROWLS sliders default to 0/10 (was 5/10 — anchored everyone
to "average" and the reviewer could save without engaging).
- Trim APPROVAL_ROLES to ['commercial', 'solution'] (was 5 roles).
- Stage 6 confirm dialog only fires on re-run, not first run.
- "+ Add manually" → "+ Add deliverable manually" with helper text.
- Asset normalizer prompt + post-hoc stop-list filter excluding internal
pitch artefacts (pitch decks, response decks, win-themes, etc.) that
were appearing as job routes in Stage 7.
Phase 3 (hardening):
- with_for_update() row locks on stage_machine.complete_stage and
approvals.submit_decision so double-clicks can't double-advance.
- 30s idempotency window on Stage 7 matching kick-off.
Deferred (next round): paste-link upload, single-use approval tokens,
FK indexes migration, datetime.utcnow → now(timezone.utc) sweep,
notes-owner schema change, file-extraction "unsearchable" UI badge.
Source: REVIEW-SESSIONS/Sales Op Platform Feedback NV_ 060526.xlsx
(rows R6-R48, 25+ feedback items mapped to specific stages).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OpportunityView: Complete-Stage button was gated on the URL :stageNumber
param and hid itself on bare /opportunities/:id URLs (e.g. when navigating
from the Dashboard). Now keyed off activeStage / stageState only.
- Dashboard: add per-card delete button with confirm dialog.
- Backend DELETE /opportunities/{id}: remove on-disk uploads dir as well as
the cascade-deleted DB rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace onMouseLeave with click-outside handler so dropdown stays
open while moving mouse inside it
- Email truncated with ellipsis instead of mid-character wrap
- clearCache() followed by reload() so login screen appears immediately
without manual refresh
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces logoutRedirect with clearCache() so the user returns to
the app login screen without being signed out of their Microsoft
account.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Shows initials avatar in top-right nav; click opens a dropdown with
name, email and Sign out button (logoutRedirect). Hidden in dev-bypass
mode. Fixes the missing logout affordance for SSO users.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds 7 users (6 editors + 1 admin) now that Azure AD SPA redirect URI
is confirmed by IT. Removes placeholder zlalani entry which has no
corresponding Azure account.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add config/allowed_users.yaml as the source of truth for access control
(email → role mapping, case-insensitive)
- New backend/app/services/allowlist.py loads the YAML and provides lookup()
- auth.py checks the allowlist on every SSO login; denies with 403 if not listed;
syncs AppUser.role from YAML on each login
- PyYAML added to requirements.txt
- docker-compose mounts ./config:/app/config into the backend container
- Frontend: axios response interceptor catches 403 not_allowlisted and fires
a custom DOM event; AuthProvider renders a NoAccessPage with Sign out button
- .env.example: clarify DEV_AUTH_BYPASS usage, document ALLOWED_USERS_PATH
Azure AD: add https://optical-dev.oliver.solutions/oliver-sales-ops-platform/
as a SPA redirect URI in app registration 9079054c (done separately by zlalani).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rewrote /about so first-time users land somewhere that explains the
pipeline honestly and gives them reasons to trust the output.
Layout (top → bottom):
1. Header — one line of plain English about what the platform does +
the "every AI run is grounded in your documents" claim.
2. Trust pillars row (4 cards):
- Audit trail per stage (cost + tokens stored)
- Two hard gates (named-role sign-off)
- Tested invariants (118 tests, deterministic maths)
- Cost is visible (~$1.50/run, per-panel pill)
3. The pipeline — the existing Mermaid flowchart with framing copy.
4. What each stage does — every stage as an expandable row with:
- Driver badge (AI AGENT / HUMAN / EXPORT / APPROVAL GATE /
DETERMINISTIC / PHASE 2)
- 1-sentence description
- On expand: endpoint · driver · inputs · output · "what the
agent is told" (the actual prompt rules in plain English) ·
cascade behaviour
5. What we do — and don't do — with AI: two columns spelling out
the trust contract (ground in uploaded docs, structured tool-use,
never hide costs · don't auto-complete, don't AI the maths,
don't pretend the AI is right).
6. If something looks wrong — the recovery instincts: every agent
has a Re-run, normalize is destructive by design (with warning),
ratecard maths is unit-tested, approval emails carry tokens, the
README is the source of truth.
Production build clean (tsc + vite build, 0 errors).
The agent prompts shown on this page are paraphrased from the actual
system prompts in backend/app/services/*_agent.py — same content,
human-friendly framing. The README has the verbatim prompts.
A single document a human can read to understand the whole pipeline:
- High-level: what the platform is + a 17-row stage table.
- Quick start (local dev) + smoke test commands.
- Deployment to optical-dev: deploy.sh flow, port-pick logic,
Apache include line, what to do on first boot vs re-run.
- Architecture summary (state machine, artifacts, approvals, cost).
- Per-stage agent reference (the heart of the doc): for each of the
9 Claude agents — what it's for, what it reads, output schema, and
the system-prompt rules in plain English. Plus the non-Claude
stages (qualification scorecard, Q&A export, ratecard build,
efficiency profile, team shape) explained the same way.
- Cross-cutting concerns: cost tracking, approvals + Mailgun, auth
paths (dev-bypass vs Azure JWT), exact stage-machine rules,
destructive-cascade rules per stage.
- Testing: how to run the 118-test suite + what each file covers.
- Repo layout.
Goal: someone landing on the repo can read this front-to-back in 10
minutes and know what every stage does, what each agent is told, and
how to run / deploy / test the thing.
Vite inlines import.meta.env.VITE_DEV_AUTH_BYPASS at build time. The
build container wasn't getting it, so even with DEV_AUTH_BYPASS=true on
the backend the SPA still rendered the MSAL login gate.
Resolve VITE_DEV_AUTH_BYPASS, falling back to DEV_AUTH_BYPASS if it's
not explicitly set, and pass it through `docker run -e` to the build.
A single DEV_AUTH_BYPASS=true in .env now controls both halves.
The script bailed when 8003 was taken on the dev server. Per spec, it
should never block on a port clash — find a free port and run with it.
How it picks ports:
- Reads OSOP_DB_PORT / OSOP_REDIS_PORT / OSOP_BACKEND_PORT from .env,
falling back to defaults 5435 / 6380 / 8003.
- For each, if the preferred port is taken on the host, scans upward in
a sane range (5435-5499 / 6380-6399 / 8003-8099) for the next free one.
- Persists chosen ports back to .env via an idempotent KEY=VALUE upsert,
so subsequent deploys keep using the same allocation.
- If our compose project is already running, skips the scan and reuses
the current ports (re-deploy in place).
Compose port mappings now reference those env vars with defaults:
127.0.0.1:${OSOP_DB_PORT:-5435}:5432, etc.
Apache config templating:
- deploy/apache-osop.conf.tmpl has __BACKEND_PORT__ placeholder.
- The script renders it to deploy/apache-osop.conf each run with the
chosen backend port substituted in. The rendered file is gitignored
(the template is the source of truth in git).
- If the backend port changed (or the Apache vhost doesn't yet Include
our conf), the script tells the user to reload Apache.
This means a fresh server hits the conflict on 8003 (something else is
listening), the script picks 8004 silently, writes it to .env, renders
apache-osop.conf with 8004, brings the stack up, and tells you to
reload Apache. Re-running the script on the same server keeps 8004.
The port-conflict check called lsof inside a pipeline. When no process
is listening (the success path on a fresh server) lsof returns 1, and
under `set -euo pipefail` that killed the script silently right after
the "Checking host ports" line.
Wrap the lsof/ss invocations in `{ … || true; }` and the call site
with `|| true`. Switched the function to `printf` so we don't get a
stray newline when the port is free.
The dev server runs everything else (/gsb/, /olivas/, /cc-dashboard/,
etc.) the same way: backend in Docker on a 127.0.0.1 port, frontend
built once and served by Apache as a static SPA via Alias. This commit
restructures V2 to match.
Apache (deploy/apache-osop.conf):
- Drops the ProxyPass-everything model; uses Alias + Directory + SPA
RewriteRule, identical to the /gsb/ block.
- Stays as an Include — drop a single line into the merged vhost:
Include /opt/oliver-sales-ops-platform/deploy/apache-osop.conf
Compose (docker-compose.yml):
- Frontend service moved behind a "dev" profile so it doesn't start in
production. Locally: COMPOSE_PROFILES=dev docker compose up
- Backend / db / redis are unprofiled and always come up.
Deploy script (deploy/deploy.sh):
- Sanity, port-conflict (5435/6380/8003 only — frontend port no longer
needed in prod), git pull, docker compose build + up.
- New step 5: builds the Vite SPA in a one-shot node:20 container and
rsyncs (or cp -a) the dist/ output to /var/www/html/oliver-sales-ops-platform/.
Uses sudo if the dest isn't writable as the deploy user.
- Reports the Apache include line + reload command.
- New flag: --no-frontend (skip the build-and-sync step).
- Unsets COMPOSE_PROFILES so the dev profile never activates on prod.
Frontend type fixes (caught by `tsc && vite build`, hidden by dev mode):
- types/index.ts gains ClientAsset / Match / MatchConfidenceKey /
RatecardLine / RatecardSummary, and StageArtifact gains its three
cost columns. These were referenced from api/assets.ts and several
Stage panels but were missing on the export side.
- New src/vite-env.d.ts declares ImportMetaEnv (VITE_DEV_AUTH_BYPASS).
- Drops one unused import (Stage4QAPack: downloadQAPack) and one
unused variable (Stage16Delivery: currentBadge).
- Production build now passes: tsc clean, vite build clean, dist/
index.html correctly references /oliver-sales-ops-platform/assets/...
Server layout (deploy server):
/opt/oliver-sales-ops-platform/ — repo + compose
/var/www/html/oliver-sales-ops-platform/ — built SPA
Apache vhost includes deploy/apache-osop.conf
Backend at 127.0.0.1:8003, db 5435, redis 6380 — none clash with the
/gsb/ stack (5432/8002/3010) or any other listed in the merged vhost.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three things needed for the optical-dev rollout.
1) Path migration /osop/ → /oliver-sales-ops-platform/
The public URL is https://optical-dev.oliver.solutions/oliver-sales-ops-platform/
Updated the basename across:
- Vite base + proxy match
- React Router basename
- axios baseURL
- MSAL redirectUri (preserves SSO when wired)
- downloadQAPack URL
- Backend app_path_prefix default
- docker-compose APP_PATH_PREFIX default
- Apache Location blocks
2) DEV_AUTH admin user
Auth middleware now reads DEV_AUTH_EMAIL / DEV_AUTH_NAME / DEV_AUTH_ROLE
when DEV_AUTH_BYPASS=true (defaults preserve the old dev@localhost /
editor behaviour). The dev_bypass identity also promotes existing
AppUser rows to admin if the env says so — so no manual SQL on the
server when we want a different account exposed.
New backend/scripts/seed_admin.py is idempotent and runs from
start.sh after Alembic migrations. It upserts the configured
DEV_AUTH_EMAIL with role=admin (or whatever DEV_AUTH_ROLE says).
Smoke-tested locally: /api/users/me now returns
admin@oliver.agency / role=admin.
3) Deploy assets under deploy/
- apache-osop.conf — drop-in vhost block (Location /…/api/ → 8003,
Location /…/ → 3011, ProxyTimeout 300, redirect bare prefix to /).
Sits alongside the existing /gsb/ V1 block on the same vhost.
- deploy.sh — idempotent script:
* sanity (.env present, docker on PATH)
* port-conflict check (5435 db, 6380 redis, 8003 backend, 3011
frontend) — if our compose project is already running, skips
the lsof check because the ports are ours; otherwise warns and
exits if anything else is listening
* git pull --ff-only (skip with --no-pull)
* docker compose build && up -d (skip with --no-build)
* health-poll backend /api/health for up to 60s
* frontend probe at the new path
* prints local + public URLs and the admin email on success
V1 host ports (5432/8002/3010) and V2 host ports (5435/6380/8003/3011)
are non-overlapping by design, so both stacks coexist on the same dev
server. CLAUDE.md naming policy is satisfied — docker-compose.yml has
name: oliver-sales-ops-platform pinned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end test (a243d6fa) on a deliberately tricky brief surfaced two
silent failures in the agent layer:
1. Diagnose Agent stuffed every clarification into the ambiguities[]
field and returned clarifications=[]. Stage 5 then had nothing to
ingest. Fix:
- clarifications gets minItems=6 / maxItems=25 in the tool schema.
- System prompt now distinguishes ambiguities (observations) from
clarifications (the actual *questions* to ask the client) and
mandates that every ambiguity must reappear here as a question-
marked sentence.
- max_tokens 4096 → 8192 (the schema is heavier now).
2. Support Docs Agent only returned summary/caveats/assumptions —
slas and kpis were truncated. The fields existed in the schema but
weren't required, max_tokens=3072 was the bottleneck. Fix:
- slas and kpis get minItems=3, kpis added to required[], schema
descriptions made explicit ("MUST be populated").
- max_tokens 3072 → 8192.
Verified on the Versuni opportunity:
Diagnose: clarifications_seeded 0 → 15.
Support docs: slas/kpis/governance 0/0/0 → 11/11/11.
Two pre-existing observations from the same E2E run (intentionally NOT
patched, documented in the test agent's report):
- Stage progression is lazy — agents accept calls regardless of stage
state; only stages/N/complete enforces ordering. This is by design
for now (lets users iterate out of order before "closing" a stage).
- Cost stamps were $0 for older artifacts (pre-cost-tracking commit
b41e399). Re-running any agent overwrites with a properly-stamped
artifact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 15 — Pitch Deck Agent
Backend:
- New service pitch_deck_agent.py: Claude reads every upstream artifact
(intake, diagnosis, qualification, delivery model, capability gaps,
support docs) plus the normalized assets list, and produces a
slide-by-slide outline via the submit_pitch_deck_outline tool. Each
slide has section, title, key_points (3-6 bullets), speaker_notes,
and an optional data_callout for headline numbers.
- Cost stamped on the artifact like every other Claude agent.
- POST /opportunities/{id}/pitch-deck — runs the agent.
- GET /opportunities/{id}/pitch-deck/outline-markdown — renders the
saved outline to a printable markdown doc.
- The original pitch_deck_stub markdown route is preserved as a
"quick auto-composed" fallback.
Frontend (Stage15Pitch):
- Headline + deck_summary cover panel.
- Slide-by-slide outline with per-section colour coding (cover /
context / approach / scope / team / commercials / governance /
next_steps), data callouts surfaced as bordered chips, speaker
notes collapsed into an italic block per slide.
- Two download buttons: structured outline markdown, and the legacy
auto-composed quick deck.
Stage 16 — Implementation Plan (post-win)
Backend:
- New service implementation_plan_agent.py: Claude composes a phased
rollout (3-6 phases with timeframe, objectives, milestones, owner),
per-market rollout sequence, training & adoption items, compliance &
policy items, in-flight metrics, and risks.
- Hard-gated: raises ValueError unless deal_status is WON, mapped to
400 by the route. Increased max_tokens to 8192 because the first
pass hit 4096 and got truncated.
- POST /opportunities/{id}/implementation-plan.
Frontend (Stage16Delivery):
- Deal-status switcher (Active / Won / Lost / Deprioritized) at the
top, persists via PUT /opportunities/{id}.
- Active: prompt to mark Won or Lost first.
- Won: implementation-plan agent runner + structured render of the
result (summary, phases with owner badges, market rollout table,
training/adoption, compliance, in-flight metrics, risks).
- Lost / Deprioritized: lessons-learned textarea (Phase 2 will wire
the save endpoint — currently note-taking only).
Smoke-tested against opp #2 (Versuni): pitch agent produced "Versuni
2026 Always-On: A Hybrid AI + Craft Engine for Philips and Senseo
Across EMEA" headline + 8-slide outline; flipped deal_status to won
and the implementation plan returned a 6-week ramp / Q3-Q4 2026
fast-follower rollout / Q1 2027 steady-state plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three things in one commit because they share the same plumbing.
1) PER-STAGE COST DISPLAY
Migration 0006 adds cost_usd / input_tokens / output_tokens columns to
stage_artifacts. Every Claude-driven agent now stamps its own run cost
on the artifact it produces:
- intake_agent (Stage 1)
- diagnosis_agent (Stage 2)
- asset_normalizer (Stage 6)
- ai_matching (Stage 7) — accumulates across the per-asset loop and
persists a NEW summary artifact (artifact_type='matching_run')
with the run totals + assets_matched + matches_created counts
- delivery_model_agent (Stage 9)
- capability_gap_agent (Stage 12)
- support_docs_agent (Stage 13)
A new <AgentRunCost> component renders a compact pill (label, cost,
in/out tokens) sourced from the most-recent stage artifact. Embedded
in the header of every Claude-driven stage panel: 1, 2, 6, 7, 9, 12, 13.
Per-deal cumulative cost still on the Stage 8 stats card.
2) STAGE 10 FRONTEND
Wires the Stage 10 efficiency-profile endpoint shipped in 2eb0422 to
a real UI (was placeholder before). Scenario picker (conservative /
moderate / aggressive), blanket slider, per-discipline overrides
(disabled when blanket > 0), tools-applied chips, free-text notes,
live impact preview that hits team-shape with the active settings,
and a Save button that persists to the artifact. Hydrates on mount
from the most recent saved profile.
3) STAGE 8 EMPTY STATE
"No ratecard yet" is now smart: when there ARE selected matches at
Stage 7 it surfaces the count + a primary "Build ratecard from N
matches" action, instead of telling you to do something you've
already done.
Smoke-tested: opp #2 ratecard rebuilt (35 lines); Stage 9 delivery
agent re-run shows $0.0437 / 4994 in / 1915 out on the badge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 stub for the proposal deck. Pulls every upstream artifact
together into a 5-section markdown doc:
1. The opportunity at a glance — intake summary + brands + delivery model
2. What's being asked for — channels, markets, capabilities, deliverables, ambiguities
3. Why we're a fit — qualification % + recommendation + delivery model + per-stage split
4. Commercial framing — caveats, assumptions, KPIs, capability gaps
5. AI cost so far — running total + call count
GET /opportunities/{id}/pitch-deck/markdown returns the .md file with the
right Content-Disposition. Saves a stage_artifact (type='pitch_deck_markdown').
Phase 2 will swap this for a python-pptx deck assembled from a template
library (the per-section content stays the same — just the rendering
changes).
Smoke-tested: 6.5KB doc on the Versuni opportunity, all 5 sections
populated from real artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 10 was previously implicit — the per-discipline override sliders in
Stage 11 fed straight into team-shape calculations without leaving an
audit trail. This adds:
- POST /opportunities/{id}/efficiency-profile — saves the active
scenario / blanket pct / discipline overrides / tools applied / notes
as a stage_artifact (type='efficiency_profile').
- GET /opportunities/{id}/efficiency-profile — returns the most recent.
The payload shape is loose by design ({scenario, blanket_pct,
discipline_overrides, tools_applied, notes}) so the Stage 11 UI can
evolve without a migration. The artifact is the audit trail; the live
calculation still runs on the query params passed to GET /team-shape.
Smoke-tested against opp #2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generates the support docs that anchor the proposal so sales promises
match delivery reality. Reads the upstream artifacts (Stage 2 diagnosis,
Stage 9 delivery model, Stage 12 capability gaps) and produces a
structured set of caveats, assumptions, SLAs (V1/V2/V3 turnaround days +
responsible party), KPIs (metric / target / measurement), governance
clauses, and a one-paragraph framing summary.
The system prompt bakes in the PARASOL Studio SLA defaults (24-48h brief
ack, 2 rounds standard, V1/V2/V3 day patterns per asset class) and
default scope exclusions (third-party fees, talent, music licensing,
shoot/production). Phase 2 will switch to a real template_library
sourced from PARASOL_Studio_SLAs_2024_V1.pptx; Phase 1 generates from
scratch with those defaults baked into the prompt.
Saved as stage_artifact (type='support_docs').
Frontend (Stage13SupportDocs):
- Run / re-run agent button.
- Summary card.
- Caveats / assumptions / governance list panels (colour-coded).
- SLA table (Deliverable / V1 days / V2 days / V3 days / Owner / Notes).
- KPI cards (Metric / Target / Measurement).
Smoke-tested against the Versuni opportunity: agent produced caveats
including "Pricing assumes 200 social posts/month and 80 eCom
assets/month as gross output volumes; retailer-specific variants
beyond 2 cuts per eCom asset…" — pulling forward concrete numbers from
the upstream stages exactly as designed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 9 — Delivery Model (services/delivery_model_agent.py)
- Reads the Stage 2 diagnosis and recommends headline traditional /
AI-supported / hybrid + a per-workflow-stage breakdown (manual /
ai_supported / fully_automated) with tooling and rationale, plus
tooling caveats and risks.
- Tool capability cheat-sheet baked into the system prompt: Pencil
(statics only), Creative-X (brand QC), Semblance (video edits),
OMG (DCO), Google Vids/Synthesia (simple internal motion),
origination/print/TVC stay manual. Agent doesn't oversell AI.
- Saved as stage_artifact (type='delivery_model').
Stage 12 — Capability Gaps (services/capability_gap_agent.py)
- Reads the Stage 2 diagnosis and returns: core_in_scope (OLIVER's own
capability list), gaps (capability + criticality red/amber/green +
suggested_source internal_sme/brandtech_partner/external_vendor +
named partner suggestion + rationale), summary.
- Suggested partners are the actual Brandtech-group + external pattern:
Jellyfish for SEO, Gravity Road for social strategy, external prod
cos for TVC at scale, etc.
Both agents share a `_run_simple_agent` helper in api/opportunities.py
that handles the validation/ commit / artifact-id-return pattern.
Frontend:
- Stage9DeliveryModel: headline badge (TRAD / AI-SUPPORTED / HYBRID) +
per-stage cards with approach colour + tooling chips, plus tooling
caveats and risks panels.
- Stage12CapabilityGaps: core-in-scope chips, gaps list with
criticality badge + source label + named partner.
Smoke-tested: hybrid recommendation for Versuni with 7 workflow stages
breakdown; capability gaps spotted EMEA transcreation (amber, external
vendor) on top of OLIVER's core in-scope list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 6 — Asset Normalizer panel
- "Run normalizer" button (with destructive-action confirm because
re-running cascades to matches + ratecard).
- Inline edit / delete / volume / tier per ClientAsset row.
- Manual "+ Add asset" form for cases where the agent missed something.
Stage 7 — Match panel
- Per-asset card showing all candidate matches (top 3 within 5% of the
best score). Radio-button selection enforces one selection per asset
via PUT /matches/{id}/select (backend deselects siblings).
- Confidence badge (exact / close / multiple / none), 0-1 score, AI
reasoning, and any caveats are surfaced inline so the user can choose
with the same context Claude saw.
- "Run matching" kicks off the background task; the matches query
refetches every 4s while we're polling and stops automatically once
every asset has at least one candidate.
Stage 8 — Ratecard panel
- Stats row (assets priced, lines, total hours, AI cost so far on the
opportunity).
- Per-asset summary table (asset / volume / role count / total hours).
- Hours-by-line table making the bug-4 fix legible: separate Base hrs,
Vol, and Total hrs (= base × vol) columns so the per-1-asset hours
are visible alongside the project-effort total.
- Build / Re-build button; the API call invalidates the ratecard +
opportunity caches so the cost meter refreshes.
API hooks and types added under api/assets.ts and types/index.ts:
useClientAssets, useCreateAsset, useUpdateAsset, useDeleteAsset,
useRunNormalize, useMatches (with optional polling), useKickOffMatching,
useSelectMatch, useRatecard, useBuildRatecard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 0005 adds three new tables (and the matchconfidence enum):
client_assets, matches, ratecard_lines. All FKed to opportunities with
ondelete=CASCADE so re-running a stage cleanly wipes the downstream
artifacts that depended on it.
Stage 6 — Asset Normalizer:
- services/asset_normalizer.py runs Claude (full uploaded text + a hint
from the Stage 2 diagnosis) and submits a structured deliverables
list via tool_use.
- api/assets.py exposes CRUD on ClientAsset rows + POST .../normalize
to (re-)run the agent. Re-running wipes existing assets, which
cascades to matches and ratecard_lines automatically.
- Smoke-tested against the Versuni brief: 7 normalized assets seeded.
Stage 7 — AI matching:
- services/ai_matching.py is the V1 engine ported and slimmed for V2:
full GMAL catalog sent per call (~3k–20k tokens depending on whether
AI-enhanced descriptions are populated), Claude returns up to 3
candidates, top match auto-selected when score >= 0.8, alternatives
kept only when within 5% of the top score.
- BackgroundTasks runs the agent off-request so the frontend can poll
GET /matches for progress. Per-call AI cost rolls onto the opportunity.
- api/matching.py: POST /match (kick off), GET /matches, PUT
/matches/{id}/select (toggle the chosen one — auto-deselects siblings
so there's exactly one selection per asset).
- Smoke-tested: 15 matches across 7 assets, GMAL323 et al picked with
reasonable scores.
Stage 8 — Ratecard:
- services/ratecard_builder.py is the V1 builder, with the V1 hours×
volume bug-fix already baked in: total_hours stores per-1-asset
hours; volume sits on the row; aggregators multiply at read time.
- api/ratecard.py: POST /ratecard/build, GET /ratecard returns a
RatecardSummary whose total_hours is correctly computed as
sum(per_asset × volume).
- Smoke-tested: 24 lines, 5,620.5 total project hours, base × volume
displayed separately.
Tests added by the parallel test agent (commit was queued during build):
- test_qualification.py (10): TROWLS save/get, threshold boundaries
(50%/60%), Pydantic 0–10 range guards, missing-dimension 422,
qualification_score stamped on opportunity, newest-scorecard wins,
404 paths.
- test_qa_pack.py (5): Excel + Word downloads (real PK\\x03\\x04
signature, openpyxl-parseable, priority-sort verified), filename
safety against /, ?, ", 404 paths.
Suite: 73 collected, 70 passed, 3 skipped (real-Anthropic), 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 4 — backend exports the clarification questions table as either
.xlsx (openpyxl, with priority colour-coded column + free-text answer
column) or .docx (python-docx, grouped by category, RED→AMBER→GREEN
inside each group, with rationale and a labelled answer slot per
question). Both include an opportunity title, client/region header
and a generation timestamp.
Stage 4 — frontend renders the existing clarification list with summary
stats (total / red / amber / green / answered) and two download buttons
that fetch via the authed axios client (so dev bypass + MSAL tokens
behave the same as every other request) then trigger a Blob download.
Buttons are disabled when there are zero clarifications.
Stage 5 — pure frontend (no new endpoints needed; reuses
PUT /clarifications/{id}). Per-question editor with a textarea for the
client's answer, save button (disabled when text is unchanged), and
quick status buttons (Mark answered / Dismiss / Reopen). Stats card
tracks pending / answered / dismissed and a "reds resolved" counter
since RED items are the actual blockers for Stage 6 (Normalize).
Smoke tests:
- Excel: HTTP 200, 7,445 bytes, valid Microsoft Excel 2007+ file.
- Word: HTTP 200, 38,823 bytes, valid OOXML.
- Both render the 15 Versuni clarifications from earlier sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 3 backend:
- POST /opportunities/{id}/qualification — saves a TROWLS scorecard
(timing/relationship/opportunity_size/what_we_know/location/sector,
each 0-10) as a stage_artifact (type='qualification_scorecard'),
computes total/60 → percentage, derives recommendation
(proceed >=60% / slt_review >=50% / no_go <50%) and stamps
qualification_score on the opportunity.
- GET /opportunities/{id}/qualification — returns the latest scorecard
or null. Smoke-tested: scores 7+8+6+7+9+7=44 → 73% → "proceed".
Stage 3 frontend (Stage3Qualify component):
- Live total + percentage card, three-band progress bar with markers
at 50% (SLT review) and 60% (proceed), live recommendation badge.
- One slider + one note input per TROWLS dimension, plus an overall
notes textarea.
- Save scorecard, with success indicator. Re-loads existing score on
mount so re-edit is non-destructive.
- StageApprovals embedded below — qualification gate uses the same
approval workflow the rest of the gated stages use.
Stage 2 diagnosis backend tests (10 new, +1 skipped real-Claude):
- /diagnose: no-files → 400, empty extracted_text → 400.
- /clarifications: empty list, priority ordering (red→amber→green
verified by seeding out-of-order via psycopg2), 404 for non-existent
opp, cross-opportunity 404 on PUT.
- PUT clarification: client_answer flips status→answered + stamps
answered_at; status=dismissed doesn't stamp answered_at; bad status
→ 400.
Suite total: 58 collected, 55 passed, 3 skipped (real-Anthropic +
owner-notification path that needs OpportunityCreate to accept
owner_user_id). 0 failures, no app-code bugs found.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces what the Diagnosis Agent already produces on the backend.
Stage2Diagnose component renders:
- Run/Re-run diagnosis button (disabled until at least one extractable
Stage 1 file exists).
- Diagnosis summary card with complexity assessment.
- Deliverables table (name / category / volume / complexity hint).
- Chip panels for channels, markets, capabilities, KPIs/SLAs, tech
asks, timelines.
- Highlighted ambiguities + contradictions panels.
- Clarification questions list (priority badge, category, question,
rationale, status, client answer if present) — these are the rows
Stage 4 will package into a Q&A pack.
Three new TanStack Query hooks: useRunDiagnosis, useClarifications,
useUpdateClarification (the last unused for now — Stage 5 will wire it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the integration suite with coverage for the approval and
notification endpoints shipped in 3f7531a / f631dad.
test_approvals.py (9 tests):
- /api/users/me returns the dev-bypass user; /api/users includes them.
- Requesting approvals on a non-gated stage returns 400 naming the
gated stages.
- Unknown approver_user_id returns 400.
- Happy path: request -> list -> /approvals/me -> blocked stage 3
complete -> approve -> stage 3 completes, opportunity advances.
- Re-deciding the same approval returns 400.
- Reject path still blocks stage 3 complete with the same error pattern.
- Token deeplink path: GET /approvals/by-token/{token} returns the
context; bogus token -> 404.
- Invalid decision string ('maybe') -> 400.
test_notifications.py (4 active + 1 skipped):
- APPROVAL_REQUESTED notification surfaced after request_approval.
- unread-count delta after mark-one-read.
- POST /me/mark-all-read zeros the count.
- Owner-notification path is skipped with a clear marker — the create
endpoint doesn't yet accept owner_user_id and the service correctly
skips self-notification when approver == owner.
Notes:
- Test harness reuses the existing `client` and `opportunity` fixtures.
- Cascade-delete (verified via 0001_initial.py / 0004_notifications.py)
means deleting the opportunity in the fixture teardown also cleans
approvals + notifications, so no extra teardown is needed.
- Email tokens aren't exposed in the API; the test reads them via
psycopg2 against localhost:5435 (DSN overridable via OSOP_TEST_DSN).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the approval flow into the UI end-to-end.
Nav:
- NotificationBell with live unread-count badge (polled every 60s).
Click to open a popover listing the latest 50 notifications;
unread items are tinted with the OLIVER accent. Clicking an item
marks it read and routes to its link_path. "Mark all read" empties
the unread state in one call.
Gated stages (3 = qualification, 14 = approval gates):
- StageApprovals component embedded in those stage views. Empty state
prompts the user to request an approval. Form: pick role + approver
from the directory (loaded from /api/users — populated automatically
as people log in). Submitting fires the backend, which creates the
approval, queues the in-app notification and the Mailgun email
(skipped silently in dev). Existing approvals render as a list with
status badges, requested-at + decided-at timestamps, comment text,
and a deeplink to the approval page. A summary line at the bottom
tells the user when all approvals are in (and whether they all
approved or any rejected).
Approval page:
- New routes /approvals/:id and /approvals/by-token/:token (the email
link path) hit the same component. Shows opportunity context (client,
region, deadline, summary), the approval's status with timestamps,
prior decision notes if any, and — for the assigned approver or any
admin — an Approve / Reject form with a notes textarea. Reject is
disabled until notes are filled in (gentle nudge to give a reason).
After submitting, TanStack Query invalidations refresh the bell, the
stage approvals list, and the opportunity stages so the user sees
the update without a manual reload.
Wiring:
- Three new TanStack Query modules: api/approvals.ts (stage list, mine,
by id, by token, request, decide), api/notifications.ts (list,
unread-count w/ refetch interval, mark read, mark all), api/users.ts
(directory + me).
- types/index.ts gains Approval, Notification, UserBrief, APPROVAL_ROLES.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two gated stages (3 = qualification, 14 = approval gates) now have a
real workflow. An admin / orchestrator picks approvers, the backend creates
Approval rows, fires in-app Notifications, and sends a Mailgun email with
a deeplink to the approval page. Each approval gets a unique email_token
so links are per-recipient. When the approver submits a decision the
opportunity owner is notified back.
End-to-end smoke test on the Versuni opp:
1. POST /opportunities/2/stages/3/approvals — created Approval #1,
notification queued, Mailgun "skipped" (no API key in dev) and logged.
2. POST /opportunities/2/stages/3/complete blocked: "pending approvals
from commercial".
3. POST /approvals/1/decision {"decision":"approve",...} — status 'approved'.
4. POST /opportunities/2/stages/3/complete — succeeds, advances to stage 4.
Schema:
- New table 'notifications' (user_id, type enum, title, body, related FKs,
read flag, indexes on user_id+read and user_id+created_at).
- Approvals table gets email_token (unique), email_sent_at, email_to.
- Migration 0004 (filename + revision id kept short — alembic_version is
varchar(32), names longer than that crash the migration runner).
Services:
- mailgun.send_email — gracefully no-ops when MAILGUN_API_KEY is empty,
logging the would-be payload so dev environments work without creds.
Selects api.eu.mailgun.net when MAILGUN_REGION=eu.
- approval_service.request_approval — creates approval + notification +
fires email, all in one call. Email is best-effort (logs on failure
but doesn't roll back the approval).
- approval_service.record_decision — flips status, stamps decided_at,
notifies the opportunity owner if there is one.
- notification_service.create_notification — thin helper.
Auth middleware now upserts the AppUser on every authenticated request
(including the dev bypass), so logged-in users automatically appear in
the approver directory at /api/users.
API:
- POST /opportunities/{id}/stages/{n}/approvals (only stages 3, 14)
- GET /opportunities/{id}/stages/{n}/approvals
- GET /approvals/me — my pending approvals
- GET /approvals/{id} — context (approval + opportunity summary)
- GET /approvals/by-token/{token} — same context, opened by email link
- POST /approvals/{id}/decision — {decision: 'approve'|'reject', comment}
- GET /notifications/me, /me/unread-count, PUT {id}/read,
POST /me/mark-all-read
- GET /users, /users/me
Config:
- New env vars: MAILGUN_API_KEY, MAILGUN_DOMAIN, MAILGUN_FROM,
MAILGUN_REGION, APP_PUBLIC_URL, APP_PATH_PREFIX (used to build
email links). Plumbed into docker-compose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Test harness hits the live backend on http://localhost:8003 (override via
OSOP_BASE_URL) rather than spinning an in-process FastAPI app, so we test
the actual deployed surface. 33 tests pass, 1 intentionally skipped
(real Anthropic-backed intake — gated behind @pytest.mark.requires_anthropic
to avoid spending money on every run).
Coverage:
- Opportunity CRUD: create with defaults + invalid model_type, all 5
model_type values round-trip, list/get/update, full create-delete-404.
- Stage machine: 17 rows initialised, advance unlocks next, double-complete
rejected, out-of-order rejected, out-of-range rejected, notes persist.
- Stage gating: stage 3 without approvals returns 400 with "approval"/
"gated" in the detail and does not mutate state.
- File upload: .txt, .md, synthesized .docx, synthesized .xlsx all
upload + extract; .exe rejected; list/delete round-trip.
- Schema sanity: list endpoints return the expected shape.
Re-run:
python3 -m venv /tmp/osop_test_venv && \
/tmp/osop_test_venv/bin/pip install -q -r backend/requirements-dev.txt && \
cd backend && /tmp/osop_test_venv/bin/pytest tests/ -v
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 2 reads the same files Stage 1 ingested and produces a structured
brief diagnosis (deliverables, channels, markets, capabilities, KPIs/SLAs,
tech asks, timelines, ambiguities, contradictions) plus a list of
clarifications to send the client. Diagnosis is saved as a stage_artifact
(type='brief_diagnosis'); clarifications land in a new
ClarificationQuestion table that Stage 4 will package into a Q&A pack
and Stage 5 will mark answered.
New table (migration 0003): clarification_questions with priority enum
(red/amber/green) and status enum (pending/answered/dismissed),
indexed by opportunity_id and (opportunity_id, status). source_stage
distinguishes Stage-2-seeded questions from Stage-5/manual additions so
re-running the Diagnosis Agent only wipes its own seeded rows, not
client answers already on file.
API: POST /diagnose (run agent), GET /clarifications (list, ordered by
priority), PUT /clarifications/{id} (used by Stage 5 to record answers).
Smoke-tested against the Versuni sample brief: 6 deliverables, 4
channels, 15 clarifications seeded, $0.0107 per call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 1 is now fully usable end-to-end. Users drop RFP/brief docs onto an
opportunity, run the Intake Agent, and the extracted metadata is stamped
back onto the opportunity (only filling fields that were left blank, so
manual entries are never overwritten) and saved as a stage_artifact for
later auditability.
Backend:
- New OpportunityFile model + migration 0002 (file metadata + cached
extracted text + extraction error if parsing failed). Files land on
disk under DATA_DIR/uploads/opp_<id>/ — the DB row is the index.
- text_extractor service supports .docx (python-docx), .xlsx/.xlsm
(openpyxl), .pdf (pypdf, newly added), .txt/.md.
- intake_agent: Claude Opus 4.7 with submit_intake_metadata tool_use,
150k-char input cap, ISO date conversion, conservative (omit rather
than guess). Per-call AI usage rolls up onto Opportunity counters so
the dashboard cost meter reflects per-deal spend.
- POST /opportunities/{id}/files (multi-upload), GET, DELETE,
POST /opportunities/{id}/intake, GET .../stages/{n}/artifacts.
- Smoke-tested against a sample Versuni brief: extracted client, region,
brands, service types, deadline, go-live, and a clean summary.
Frontend:
- Stage1Intake component: drag-and-drop + browse upload, file list with
size and extracted-char count, per-file delete, "Run intake agent"
action, metadata card showing client/region/brands/service-types/dates
and a summary block.
- TanStack Query mutations invalidate the opportunity detail and stage-1
artifact cache so the metadata card and the opportunity header refresh
immediately after the agent finishes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creating an opportunity now initialises all 17 StageState rows in a single
transaction, with stage 1 in_progress and stages 2-17 not_started. The
stage_machine service enforces sequencing (stage N can only be completed
once predecessors are completed) and the gating rule for stages 3 and 14
(at least one Approval row, all approved, before completion is allowed).
Backend: GET/POST/PUT/DELETE /api/opportunities, GET /stages,
POST /stages/{n}/complete. The opportunity serialiser maps SQLAlchemy
enum values back to lowercase strings so the response model coerces
cleanly.
Frontend: Dashboard listing opportunities (with deal_status badge and
current-stage hint), NewOpportunity form, OpportunityView with a
horizontal StageStepper component. Stepper renders all 17 stages with
status-driven visual states (locked / in-progress / completed / awaiting
approval), highlights the gated stages (3 and 14), and links into a
per-stage view via /opportunities/:id/stage/:n. The old Home page (the
Mermaid flowchart + stage card grid) is preserved as /about.
Wired through React Router; TanStack Query handles the cache + mutation
invalidation so the stepper updates immediately after a stage completes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The full GMAL catalog now ingests cleanly into V2 — verified counts match
V1: 390 assets / 243 with hour routes / 120 roles / 8,660 hour records /
695 service lines / 165 role-level mappings.
The parser is the V1 implementation, narrowed to clear only GMAL-owned
tables (V1 also blew away projects/matches/ratecard_lines, which V2
doesn't own — opportunities live in their own state-machine tables).
Browse endpoints (/api/gmal/{assets,assets/{id},assets/{id}/family,roles,
stats}) are ported from V1 unchanged. Editor write endpoints and AI
description regeneration are deferred until the GMAL Editor UI lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end runnable skeleton for the OLIVER Sales Operations Platform —
the V2 of the Scope Builder, broadened to a complete RFP-to-mobilization
pipeline (intake → qualification → Q&A → match → ratecard → delivery
model → efficiency → team shape → caveats → approval gates → pitch →
post-win planning → downstream handoff).
Backend (FastAPI + async SQLAlchemy + Alembic):
- Models: app_users (with workflow_roles for approver routing), GMAL
catalog ported from V1 (gmal_assets / roles / gmal_hours / service
lines / role-level mappings), and the new state machine
(opportunities, stage_states, stage_artifacts, approvals).
- Initial Alembic migration creates 11 tables and 5 enum types using the
postgresql.ENUM(create_type=False) pattern so the types aren't
double-created when referenced from multiple columns.
- Claude client defaults to claude-opus-4-7 with cost tracking + debug
log; Azure SSO middleware ported as-is from V1.
- Public /api/health round-trips a SELECT 1 to verify the DB is reachable.
Frontend (React 18 + Vite + TanStack Query + MSAL + Mermaid):
- Home page renders the canonical 17-stage flowchart (Mermaid) plus an
enumerated stage card grid with the two approval gates highlighted.
- React Router uses /osop basename to mirror the V1 /gsb/ deploy
pattern; axios client targets /osop/api with MSAL token interceptor.
Compose:
- name: oliver-sales-ops-platform (so the project doesn't collide with
the deploy-folder default per shared-server policy in CLAUDE.md).
- Ports 5435 / 6380 / 8003 / 3011 to coexist with V1 on the same host.
- Source mounts on backend (app/, alembic/, alembic.ini) and frontend
(src/, configs) so dev iteration doesn't require rebuilds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>