Assistant grew remember + recall tools this loop (7 total). The previous
exact-count assertion is now an at-least check so future tool additions
don't break QA.
Sidebar now shows:
- aggregate unread count banner at top
- per-thread last-message preview (You: prefix when self-sent)
- relative timestamp on each thread (just now / Xm ago / Xh ago / Xd ago)
- amber unread dot + bold name for threads with new messages from others
Read state stored in localStorage per-user (fm.canvas.chat.read.<email>).
Auto-marked read when a thread is opened. Pure client-side; no extra EA2
writes beyond the existing listMessages calls.
src/lib/llmClient.ts: browser POSTs to /internal/canvas-llm/chat with
{messages, max_tokens}. Mirrors the normalized request shape used by
earendil-works/pi's @earendil-works/pi-ai. Returns:
- ok:true with content+provider when configured and successful
- ok:false, configured:false when proxy 404/503/unreachable
- ok:false, configured:true when provider returned an error
routeAgentInput now falls back to the LLM with a system prompt that
includes the user identity, tool catalogue, and the top-3 vault recall
hits when no deterministic tool matches. When the proxy is not
configured the deterministic vault-hint reply is used (no regression
in the demo tier).
User explicit ask: 'maybe with a miniature hindsight or a Karpathy
vault behind it as its memory system for each tenant.'
- src/lib/agentMemory.ts: EA2-backed vault per user. Vault root is
flow.kind=value at flow/memory_vault_<email_slug>; each memory is a
flow.kind=value doc linked via defines edge with role=presentation.
- agentMemory.remember/listAll/recall — recall is a simple keyword
overlap score with recency tiebreak.
- agentTools: new 'remember' and 'recall' tools.
matchers: 'remember that ...', 'note ...', 'save ...' /
'recall ...', 'remind me ...', 'what do you know about ...'
- routeAgentInput: every user turn is best-effort persisted as an
episodic memory (fire-and-forget). When no command matches, the
router does a vault recall and surfaces related notes before
showing the help message.
Vault key: deterministic per-user. Owner_email stored on the vault
root + on each memory's config.memory. No raw IDs visible to the user.
EA2_DRAFT_PROCESS:process_creation and fm06-t10-demo-reset-v2 added to
STARTABLE_SOURCE_CONTEXTS. Wizard-published flows now reach the Hub
catalogue and the Assistant's list_processes reply.
Reverse-engineered the EA2 runtime startability contract by diffing
pr_to_po_def (startable) against wizard-created drafts (500). Minimum
shape required:
Parent flow (kind=definition, status=published)
+ Step flow (kind=definition, status=published, dispatch_kind=human)
+ View doc (kind=definition, status=active, layout={fields,layout:form})
+ Version doc (kind=definition, status=active)
+ defines edge: parent -> step (role=child)
+ defines edge: step -> step (role=dispatch self-edge)
+ defines edge: step -> view (role=presentation)
+ governs edge collection 'governs': version -> parent (role=governs)
wizardApi.ops gains: createView, createVersion, childEdge, dispatchEdge,
presentationEdge, governsEdge. createStepEdge renamed to childEdge to
reflect the actual EA2 role. Steps now create with status=published
(was draft) so they're visible to the runtime.
Wizard.handleStructureSave emits all of the above in two apply-batches
(creates then wires). Verified live: posted a probe flow with this
contract, /api/runtime/transactions returned 200 + a running
transaction with step_run_id.
The /api/ea2/flow/processes endpoint caps at ~203 items and pr_to_po_def
is not in that window for this tenant despite being status=published.
fetchStartableFlows() does a direct GET per allowlist key and merges
results with the catalogue list (allowlist hits take precedence).
Wired into Hub.loadPublishedFlows + agentTools.list_processes +
agentTools.start_process.
The allowlist by _key now handles those exclusions cleanly. The bare
'demo' / 'test' / 'ea2' regexes were sweeping out pr_to_po_def itself
('Executable curated procurement process for demo.flow-master.ai reset
smoke checks.') because its description contains both 'demo' and the
old smoke[_\\s]?test pattern variant.
Previous attempt blacklisted unstartable backfill source_contexts but
swept all real published flows out too. Inverted to a positive
allowlist: only flows whose _key is in STARTABLE_FLOW_KEYS reach the
Hubs / Assistant.
Today the canonical pr_to_po_def is the only runtime-startable flow in
the tenant. New entries are added by hand once we've verified a flow
actually starts via /api/runtime/transactions. Cleaner than chasing
source_context strings.
Oracle round-6 watch-out closure. The Assistant's list_processes was
returning PO Approval + Purchase Order Approval, both of which return
500 transaction_creation_failed on /api/runtime/transactions because
they were backfilled into the catalogue without view/data attachments.
Added 5 source_context exclusions matching the seed-time markers of
those flows: D2 live backfill / EA2 runtime reference seed /
atlas-f1-fresh-proof / EA2 seed payload via POST /api/process /
fm-process-mining-conformance-reseed.
The canonical pr_to_po_def (source_context fm06-t10-demo-reset-v2,
display 'Purchase Requisition to PO') survives — it is the only
runtime-startable flow in the tenant today.
Oracle round-6 watch-out: 'Employee Onboarding' (a CANVAS_SEED_PROCESS
wizard-incomplete draft) was reaching the Assistant list because its
display_name passed the dev-artefact regex. EA2 runtime then returned
500 transaction_creation_failed because it lacked view/data attachments.
- flowCuration.NON_BUSINESS_SOURCE_CONTEXTS now drops CANVAS_SEED_PROCESS,
CANVAS_SEED_STEP, CANVAS_CHAT_INBOX, CANVAS_ATTENDANCE_ROOT,
CANVAS_ATTENDANCE_SITE - all internal canvas source_contexts.
- qa/full_dogfood.mjs new assertion 'assistant_list_processes_all_startable'
posts each Assistant-listed flow to /api/runtime/transactions and
asserts non-4xx/5xx response.
Closes Oracle deferred caveats:
- raw-SQL persona promotion (now qa/seeds/001_personas.mjs, idempotent)
- no scripted multi-persona interaction (now qa/seeds/002_persona_
interactions.mjs - HR onboarding tx, CEO budget tx, IT provisioning
tx + 3 cross-persona chat threads, verified end-to-end)
- geo-attendance mock data (now src/lib/attendanceApi.ts pulls 8 sites
via flow.kind=value + defines edges from attendance_root anchor;
qa/seeds/003_attendance_sites.mjs seeds them idempotently)
Each site is a flow.kind=value doc with config.attendance = { lat,
lng, kind, status, who, city, label }. The anchor flow + presentation
edges follow the same per-user inbox pattern that's already proven on
chat. No hardcoded JS array left in src/scenes/GeoAttendance.tsx.
EA2 doesn't expose a generic flow-by-kind list endpoint; flow/processes
is definition-only. Migrated chat to flow.kind=value, so direct listing
needed a new strategy.
- Each user has a deterministic inbox flow.kind=value doc keyed by
email slug. ensureInbox() creates it idempotently on first listThreads.
- createThread links the new thread into BOTH participants' inboxes via
defines edges with role='presentation' (each link is one apply-batch).
- listThreads(userEmail) reads defines edges from the user's inbox key.
No more reliance on source_context regex filtering through the
definition catalogue.
Updated all 3 call sites: Chat.tsx (mount + after-create refresh) and
agentTools.send_chat. 31/31 tests green.
Oracle deferred caveat #2 — chat-on-flow-collection hack — CLOSED.
Previous design used flow.kind=definition + source_context=EA2_CHAT_THREAD
for chat threads and EA2_CHAT_MSG for messages, then filtered them out of
hub/wizard surfaces via a NON_BUSINESS_SOURCE_CONTEXTS exclusion. That
was containment, not a clean model.
Verified EA2 contract: flow.kind='value' is a valid enum value (probed
against ea2.baseline.svc — kind=value returns 200, kind=conversation/
thread/message/channel/note all return 400 schema violation).
- Thread: flow.kind=value, source_context=CANVAS_CHAT_THREAD
- Message: flow.kind=value, source_context=CANVAS_CHAT_MSG
- Linked via defines edges (already validated in round 2)
Because curatedPublishedFlows already requires kind='definition', chat
docs are now invisible to business surfaces by SCHEMA, not by source_
context regex filter. The filter is kept as defence-in-depth (+ legacy
docs created during round 2 still need to be filtered out).
Tried data collection first; it requires relates(role=sourced_from) edge
to an sdx_connections handle per the P19 invariant — wrong model for
chat, so flow.kind=value is the right home.
Oracle round-5 PASS verdict was non-blocking on this. Sibling test to
the no-token regression, proving authedRequest's 401 retry path also
throws AuthRequiredError instead of silently re-logging-in via
/api/v1/auth/dev-login when VITE_ENABLE_DEV_LOGIN=false.
31/31 unit tests green.
Oracle round-4 finding: authedRequest() called the private login()
helper whenever no bearer token existed AND on 401 retry. That helper
posts to /api/v1/auth/dev-login, so any authenticated API call could
silently issue dev-login regardless of UI gating.
- authedRequest checks devLoginAllowed() (VITE_ENABLE_DEV_LOGIN !==
'false') before calling login(). When dev-login is disabled it
throws AuthRequiredError instead.
- 401 retry path gated the same way.
- New AuthRequiredError exported so callers (store, scenes) can route
unauthenticated users to the login page instead of swallowing.
- src/lib/api.test.ts: regression test 'throws AuthRequiredError
instead of silently calling /dev-login when no token'. With
VITE_ENABLE_DEV_LOGIN=false api.me() rejects with /Sign in required/
and no POST to /dev-login is observed.
30/30 unit tests green.
Oracle round-3 finding: passing password=undefined to api.signIn silently
hit /api/v1/auth/dev-login. With dev-login disabled in a production
build, a buyer typing only their email and pressing SIGN IN would still
have been authenticated as that user.
- api.passwordLogin(email, password) explicitly requires a password and
always hits /api/v1/auth/login. Throws on missing password.
- api.devLogin(email) explicitly hits /api/v1/auth/dev-login and is the
only entry point to that endpoint.
- store.loginAs(email, password, { method: 'password' | 'dev' }) routes
to the right call. Default is password (requires password).
- Login.handleSubmit rejects empty password with 'Password required'
before any network call.
- Login.handleDevLogin bails when devLoginEnabled is false.
- SSO button is disabled while probe state is 'unknown'.
- Login.test.tsx: regression test 'blocks empty-password SIGN IN —
must NOT silently call dev-login'. Plus SSO test asserts the button
starts disabled.
29/29 unit tests green.
- Login.tsx: dev-login gated by VITE_ENABLE_DEV_LOGIN (defaults on, set
to 'false' in production builds). SSO button probes /microsoft/login
and shows 'NOT CONFIGURED' or 'NOT WIRED' state with disabled action
instead of looking like a working primary login.
- Hub.tsx: 'Queue'/'Selected work' renamed 'Available workflows' /
'Workflow detail' + footnote that a real work-item queue replaces it
when the EA2 hub queue is wired. No more semantic overclaim.
- Landing.tsx: Attendance Map chip gated by VITE_ENABLE_GEO_PREVIEW
(default on) and labelled 'preview' so it can be hidden in prod
builds.
- flowCuration.ts: extend the dev-artefact filter to drop engineering
names (EA2 *, SDX *, *runtime reference*, *demo*, *test*) so Pi and
the hubs only ever expose buyer-appropriate flows.
- All 29 unit tests + 26 dogfood QA assertions still green.
Microsoft SSO endpoint exists at auth-service but returns 503
'Microsoft SSO is not configured' for the canvas tenant. Previously
clicking the button silently navigated the user to a 404 JSON page.
handleSSO now probes the endpoint first:
- 3xx -> follow redirect (real SSO flow)
- 503 -> 'Microsoft sign-in is not yet enabled on this tenant'
- 404 -> 'Microsoft sign-in is not wired to this environment yet'
- network error -> surface the reason
Closes Oracle gap #6 with truthful UX while leaving the SSO path
ready to light up once the tenant Azure AD config is in place.
Oracle remediation batch 1 — closing gaps #1 (agent overclaim), #4
(demo-data leak), #5 (raw-ID exposure):
- src/lib/flowCuration.ts: shared isDevArtefact + curatedPublishedFlows
filter for the whole codebase. Patterns drop rv_/orphan/Atlas F1/MCP
SDX/Codex Test/sidekick/process_<ts>/backfill/smoke/probe/fixture and
the EA2_* internal source_contexts.
- src/scenes/Hub.tsx: loadPublishedFlows uses the shared curator
- src/lib/agentTools.ts: list_processes and start_process both use the
shared curator; raw transaction_id no longer in start reply
- src/state/store.ts: snapshot + live merges curated; resolveDefaultStepId
guards against scenarios whose defaultStepId references a stale step
- src/scenes/Wizard.tsx: 'Process definition <hex>' confirmation replaced
with the user-facing process name
- src/scenes/Hub.tsx: start toast no longer shows transaction_id prefix
- src/scenes/Agent.tsx + App.tsx + Landing.tsx: rename Pi -> FlowMaster
Command Assistant. Welcome message says explicitly that this is a
deterministic router and the LLM agent + tenant memory are on the
roadmap. Tab label, hub chip, turn author all rebranded.
- src/scenes/GeoAttendance.tsx: scene re-titled to 'Attendance map ·
preview' with honest copy explaining the dataset is illustrative until
the EA2 attendance feed is wired in.
- src/data/synthetic.ts: orphaned from runtime (kept in tree for tests).
Snapshot now sources only the live-cached EA2 read (scenarios.json),
filtered through the same curator.
29/29 tests green. No more 'rv test flow 0' / 'orphan' / 'Atlas F1' / raw
32-char hex IDs visible to a buyer.
Single QA script (qa/full_dogfood.mjs) drives the live URL like a real
operator and asserts all of Round 2:
auth_guard_unauthed_redirects_to_login
login_shows_three_personas
login_has_microsoft_sso_button
ceo_persona_dev_login_lands_on_landing
landing_exposes_all_hubs_and_extras (7/7)
explainer_renders_eight_cards
geo_attendance_renders_tiles + 8 markers
pi_agent_shows_five_tools
pi_agent_list_processes_returns_real_ea2_data
procurement_hub_lists_real_flows
chat_scene_loads_with_thread_sidebar
theme_toggle_flips_theme
security_header_* x6 (HSTS, X-Frame-Options, X-CTO, Referrer-Policy,
CSP, Permissions-Policy)
Exits non-zero on failure so the script is CI-grade.
Eight-card explainer covering Business-as-Code philosophy, the five
element types (Flow/Data/View/Rule/Version), the DEFINE/VERSION/DEPLOY/
EXECUTE lifecycle, hubs, work items, EA2 graph storage, and the Pi
agent. Sourced from FlowMaster Element Architecture v2.1 + the EA2
collection/edge contracts recalled from Hindsight today.
New 'What is FlowMaster?' chip on Landing routes here.
New GeoAttendance scene plus Attendance Map chip on Landing. Uses
react-leaflet 4 + leaflet 1.9 + OSM tiles (zero-key OSS) to render eight
seed sites (HQ + stores + offices + warehouse) with per-marker check-in
status (on-site / late / off-shift), kind filter (all/store/office/
warehouse), and a People Hub jump-off. Auto-fits to the visible markers.
OSS stack: react-leaflet@4, leaflet@1.9.4 (battle-tested by tens of
thousands of production deployments), default marker images served from
unpkg to avoid bundler asset hassle.
- loadPublishedFlows: drop source_context EA2_CHAT_THREAD/_MSG and
EA2_WIZARD_STEP so chat threads and wizard fragments don't leak into
the business catalogue
- procurement match: require word boundaries on PO/P2P; add requisition/
invoice
- HR match: word-bound HR; add offboard/employee/payroll
- IT match: word-bound IT; explicit access/laptop/password tokens; drop
the over-broad bare 'service' and 'account' tokens
Single Hub component parameterised by hub key. Each hub:
- Lists quick actions that look up a matching published process and
start it as a real transaction
- Pulls the full published catalogue from /api/ea2/flow/processes and
filters via a hub-specific regex (procurement|hr|it)
- Falls back to top-N when no matches, with a Studio CTA when empty
Landing surface gets a hero-hub row of chips: Procurement Hub, People
Hub, IT Hub, Talk to Pi, Team Chat. Same EA2-write doctrine as wizard.
New Pi tab. Talks to a typed tool router that maps natural-language
intent to real EA2 calls:
navigate go to mission / runs / studio / chat / hubs
start_process 'run laptop procurement for store-204'
send_chat 'tell Mariana that the quote is approved'
create_process opens the wizard
list_processes pulls published flow definitions for the tenant
This is the OSS-pluggable shape: today the router is rule-based and
fully wired to FlowMaster; swapping in an LLM provider for intent
parsing is a localised change in routeAgentInput().
process-definitions/{key}/graph filters to published process definitions
and returns empty for chat-thread flows, so the second persona saw no
message bodies. The raw defines edge endpoint returns the link correctly.
New Chat scene at the Chat top-bar tab. Threads and messages are stored
as flow docs (kind=definition, source_context=EA2_CHAT_THREAD / _MSG)
linked by defines edges with role=next — same proven contract as wizard
steps. Persona directory lets the signed-in operator open a thread with
the CEO / HR / IT personas.
- src/lib/chatApi.ts: listThreads / createThread / listMessages / sendMessage
- src/scenes/Chat.tsx: sidebar (threads + new-thread picker) + main pane
(header, bubbles, composer with Enter-to-send / Shift-Enter newline)
- src/index.css: chat-scene grid + bubble + composer styles
- src/App.tsx: Chat tab + scene route
Seeded HR/CEO/IT directors as real superuser rows in auth_service.user via
auth-service POST /api/v1/auth/register + a one-shot SQL promotion to
is_superuser=TRUE so dev-login resolves them:
ceo-head@flow-master.ai → Mariana Cole (Chief Executive)
hr-head@flow-master.ai → Aisha Khan (HR Director)
it-head@flow-master.ai → Rohan Patel (IT Director)
Login page exposes them as three QUICK PERSONA chips that prefill the
operator-id field; clicking SIGN IN AS DEVELOPER then issues a real JWT
for that persona. No mock users.
EA2's apply-batch doc collections are restricted to ['data','flow','rule',
'version','view']; there is no 'ea2_node'. Steps are themselves 'flow'
documents (kind='definition') linked to the parent flow via the 'defines'
edge collection with role='next' (or 'presentation').
handleStructureSave now does two batches:
1) create N step flows, capture server-assigned keys
2) wire parent->step1->...->stepN with create_edge on 'defines'
Wizard was sending ops in the SDX-era shape:
{ collection, op: 'insert', flow_key, _key, ... }
with actor + flow_key duplicated at the request root.
Real EA2 contract (verified against ea2.baseline.svc/openapi.json):
POST /api/ea2/apply-batch
body: { request_id (8..128), ops: BatchOp[] (1..500), description? }
additionalProperties=false at request root
ops are a discriminated union by 'op':
create / update / delete (nodes by coll+key+data)
create_edge / delete_edge (edges by edge_coll+from+to)
actor/tenant resolved from Bearer token, not body.
Adds typed CreateOp/UpdateOp/DeleteOp/CreateEdgeOp/DeleteEdgeOp + wizardApi.ops
helpers (createNode, createEdge, publishFlow) so call sites don't have to
remember the discriminator shape.
Wizard.handleStructureSave + handlePublish updated to use the helpers.
Backend EA2 apply-batch requires a request_id (idempotency key). Without
it, every node/edge write after Intake was failing with HTTP 422 and the
wizard stayed stuck on the Analyze phase.
Verified via qa/audit_wizard_writes.mjs against the live URL: previously
1 successful POST (draft creation) + 6x 422, now full multi-phase
write path enabled.
Previously asserted post-click theme must equal 'dark', which was
backwards because default theme is dark — clicking flips to light.
Now record before, click, record after, assert they differ.
Also tighten the selector to .theme-toggle (which is the class on
both Landing and Topbar toggle buttons).
Two bugs:
1. [data-theme="dark"] block was defined BEFORE :root in source CSS,
so :root cascade overrode it. Moved to AFTER :root and bumped
specificity to html[data-theme="dark"] to beat plain :root.
2. Theme toggle button only existed in the topbar, which is hidden
on landing/login/sso-callback scenes. Added a ThemeToggle to
the Landing header so users can flip theme from the home page.
Confidence: high
Scope-risk: narrow
Previous topbar theme button used useApp.getState() inline in JSX
which doesn't subscribe — clicking changed state but the icon never
re-rendered. Replace with proper component that subscribes via hook.
Also add visible 'DARK'/'LIGHT' label so the button is discoverable
without hover-tooltip-only labelling.
Confidence: high
Scope-risk: trivial
The Vite minifier was tree-shaking the dev-login button because
devLoginConfig() always returned {enabled: false} when the
/internal/dev-login-config endpoint 404'd (which is the case on
canvas, where the endpoint isn't mounted).
That meant my prior 'default ON' fix never took effect — the useEffect
unconditionally flipped state to false within microseconds of mount.
Real fix: return {enabled: null} on absence. Only flip OFF when
endpoint explicitly says enabled=false. Otherwise honor the source
default.
Confidence: high
Scope-risk: narrow
The /internal/dev-login-config endpoint is a fm-shell Next.js internal
route that doesn't exist on the canvas nginx. Without that endpoint
the dev-login button was hidden, which contradicts the explicit user
ask: 'dev-login button is required to be there until we cut over to SSO'.
Fix: default state to true, treat the config endpoint as 'flip OFF only'.
If the endpoint returns enabled:false we hide. Otherwise (404, network
error, etc.) we keep the button visible.
Confidence: high
Scope-risk: narrow
Not-tested: real /internal/dev-login-config returning enabled:false (would
need that endpoint mounted on canvas — out of scope for now)
Runtime-only Dockerfile.runtime copies a pre-built dist/ into the nginx
image; sidesteps the Node-on-emulation libuv crash when building on
Apple Silicon for linux/amd64.
nginx.conf hardened:
- HSTS, X-Content-Type-Options, X-Frame-Options DENY, Referrer-Policy
- Permissions-Policy locking down camera/microphone/geolocation/payment
- Content-Security-Policy with strict default-src self + connect-src
scoped to our backend
- COOP / CORP same-origin
- X-Robots-Tag noindex (not a public marketing site)
- server_tokens off
Confidence: high
Scope-risk: narrow
Every scene now reads as the same FM doctrine: paper canvas + navy
frame + amber accent, 1px hairlines, square edges, monospace
uppercase labels, no glass, no shadows, no gradients.
WHAT CHANGED
- src/index.css rewritten end-to-end. Doctrinal hex tokens declared
at :root, legacy --bg/--surface/--text/--primary/--border aliases
repointed at doctrine values so existing component classes inherit
the blueprint palette without per-component churn. Global
* { border-radius: 0 } + box-shadow strip-out. All low-opacity
tints via color-mix(in srgb, var(--bp-navy) X%, transparent) so no
rgba() literals survive.
- Topbar redone as a 44px instrument strip: navy brand-lock with
amber mark, uppercase mono tabs, amber-on-paper selected tab,
square link buttons, mode pill with currentColor border.
- Mission Control hero + scenario tab strip + KPI cards retoken
with amber underline on selected.
- Left rail: 1px-bordered KPI grid, queue cards stack as a single
bordered list, agent supervision actions span the full width.
- Inspector: tabbed nav with amber selected, hairline-separated
fields, square rule + run + evidence cards.
- Command palette: paper bg, amber-bordered selected item, mono
caps headings.
- Live API console: paper drawer, mono call rows, amber filter chip,
color-tokenised METHOD_COLOR map.
- Toaster: left-border accent on paper surface.
- Telemetry: navy gauges, mono row, square tick.
- Studio + Settings: shared studio-grid layout — paper panels in a
1px navy grid with no margins between, mono inputs.
- Run History: mono table rows with amber selected filter chip.
- Landing: mono hero, square brand mark, stats strip as one
bordered row, scenario cards in a 1px grid (no glow, no shadow,
no gradient).
- Family accents in synthetic.ts and buildScenarios.ts retoken to
doctrine hex. src/scenarios.json snapshot patched in place.
- Console.tsx METHOD_COLOR map → var(--bp-muted/info/amber/err).
AUDIT
- qa/palette_audit.mjs upgraded: scans 31 source files (.ts/.tsx/
.json + index.css), catches rgb()/rgba()/hsl()/hsla() literals,
and refuses named CSS colors (white/red/blue/...) as background/
color/fill/stroke/border values. Hex regex uses (?![0-9a-fA-F])
lookahead so deploy-id text like '#d3f1a' is not a false positive.
Result: 0 non-doctrinal literals anywhere in src/.
- qa/smoke.mjs + qa/smoke_blueprint.mjs hasText matches converted
to case-insensitive regex because the doctrine uppercases every
user-visible label via text-transform: uppercase.
- qa/snap_all_scenes.mjs captures 9 fresh 1440x900 screenshots
in qa/screenshots/v4/ (landing, mission procurement, mission AR
blueprint, inspector raw, command palette, studio, settings,
run history, mission live with console).
VERIFY
- tsc -b clean
- vite build green (CSS 41 KB / 8 KB gz, JS 851 KB / 230 KB gz)
- vitest 5 files / 24 tests green
- main smoke 27/27, 0 console errors
- blueprint smoke 15/15, 0 console errors
- palette audit clean (31 files)
ORACLE-REVIEWED
Round 1 PASS with <promise>VERIFIED</promise>. Two non-blocking
audit hardening notes landed in this same commit: scan JSON,
catch rgb/hsl/named CSS colors.
Confidence: high
Scope-risk: moderate (theme sweep, all scenes touched)
Not-tested: pixel-level visual diff vs prior theme (Oracle could
not inspect images this round; relied on programmatic checks)
Pulls the actual FM06/flow-master-design-philosophy doctrine
(DESIGN_PHILOSOPHY.md + SYNTHESIS.md + IMPLEMENTATION_STANDARD.md
+ ADR 0001/0002) and rebuilds the canvas to match: 'operations
cockpit', 'industrial, instrumented, accountable'. Light paper
canvas + navy frame + amber accent + 1px rules + square edges +
monospace operational labels.
WHAT CHANGED
- ProcessGraph.tsx rewritten: square 1px navy nodes, mono uppercase
labels, orthogonal step edges (ReactFlow type:'step' → MLHV only),
two-layer Background (8px minor + 64px major navy hairline grid),
doctrinal palette tokens via var(--bp-*).
- BlueprintFrame.tsx (new): top instrument readout strip
(DEF / VERSION / HUB / NODES / EDGES / SRC / MODE / TX), top + left
rulers with 8/64px ticks, navy corner glyph at origin, bottom
status legend.
- index.css: scoped [data-canvas="blueprint"] block (~280 lines)
declaring 11 doctrinal hex tokens once; opacity derivatives go
through CSS color-mix(in srgb, var(--bp-navy) 13%, transparent)
not raw rgba(). No box-shadow on selected node (outline instead).
- LeftRail.tsx: gate toast now names the real endpoint
(POST /api/runtime/transactions/{id}/actions/{submit,save_draft})
on the unsigned-in path too, restoring the convention from the
prior real-mutations pass.
- qa/smoke.mjs updated for the new selectors (.bp-node,
.bp-readout-blueprint). Old guided-tour assertions replaced with
Studio scene assertions. 27/27 PASS.
- qa/smoke_blueprint.mjs (new): 15 assertions covering S1–S3 + S5.
- qa/palette_audit.mjs (new): three checks — doctrinal CSS hex,
no raw rgba() in blueprint scope, no hardcoded color literals in
blueprint TSX. All pass.
ORACLE-REVIEWED
Round 1 FAIL: hardcoded TSX color literals + box-shadow + rgba +
narrow palette audit. Round 2 PASS after fixing all four.
CONTRACT EVIDENCE
- vitest: 5 files, 24 tests, all green
- main smoke: 27/27, 0 console errors
- blueprint smoke: 15/15, 0 console errors
- palette audit: 11 CSS doctrinal hex tokens, 0 raw rgba, 2 TSX files clean
- vite build: green
Confidence: high
Scope-risk: narrow (scoped [data-canvas="blueprint"])
Not-tested: pixel-level visual diff (Oracle could not inspect images
this round; relied on programmatic DOM + path-command assertions)
Massive overhaul that turns the demo from presenter-mode into a
fully-functional FlowMaster operator surface.
NEW: real backend mutations
- api.executeAction(txId, actionId, actor, values) → POST /api/runtime/transactions/{tx}/actions/{actionId}
- api.startTransaction(defKey, business_subject) → POST /api/runtime/transactions
- api.createProcess(payload) → POST /api/ea2/flow
- store.executeAction / startInstance with toast feedback + auto-refresh
- Inspector Overview action buttons fire real backend calls (Submit/Save Draft/etc)
- LeftRail Confirm/Reject buttons fire real backend calls
- LeftRail 'Start new instance' button starts a real tx for live procurement
- CommandBar 'Real actions' group with 'Start new instance' + 'Execute action on headline tx'
NEW: Process Studio (src/scenes/Studio.tsx)
- in-UI process designer: name + display + hub + description + node list + edge list
- live JSON preview of the EA2 payload
- Publish button calls api.createProcess against demo.flow-master.ai
- Validates locally before publishing
- Auto-refreshes scenarios after publish so the new process shows up
NEW: Settings (src/scenes/Settings.tsx)
- Identity: sign in as any email (loginAs), see actor + display name
- Quick-user buttons for common demo identities
- Backend URL + clear-token diagnostic
- Polling cadence (2-120s)
- Dark/light theme toggle (CSS data-theme attribute)
- Show-console default toggle
- All persisted to localStorage (LS_KEY = fm.mc.prefs.v1)
NEW: Live API console (src/components/Console.tsx)
- Right-side drawer triggered from topbar
- Every fetch (GET/POST/etc) streams in real time with status + duration
- Click any entry to expand request + response JSON
- Filter: all / writes / errors
- Replaces the old guided-tour overlay entirely
NEW: live polling
- store.startPolling()/stopPolling() with setInterval guarded for SSR
- Auto-refresh while in LIVE mode at configurable cadence
REMOVED: Tour.tsx, all startTour() store actions and tour references
- Landing CTA now reads 'Enter Mission Control' / 'Design a process' / 'Open live console'
ALSO:
- api.ts: instrumentedFetch with observer pattern → store.apiLog
- Topbar: user identity chip linking to Settings, Console toggle with badge
- Light theme: minimal CSS data-theme override (text + surfaces only)
- localStorage persistence for mode, scenarioId, email, theme, pollEverySec, consoleOpen, recents
- 24/24 vitest, smoke quick run shows 0 console errors + 7 API calls captured
Confidence: high
Scope-risk: broad (~14 files)
Not-tested: actual end-to-end backend mutation roundtrip (requires LIVE + sign-in; structure proven via probe scripts)
Oracle r4 'Watch Out For' caught a real edge case: if a backend graph
changes shape while the scenarioId stays the same, the prior
selectedStepId could survive the merge and point at a step that no
longer exists. Two new vitest regressions in state/store.test.ts now
pin the contract:
- refreshLive() resets selectedStepId to the scenario's defaultStepId
when the prior step no longer exists in the merged catalog
- refreshLive() preserves a still-valid selectedStepId
runLiveFetch() now derives stepStillThere from the merged scenario's
own steps (not the old store) and falls back to defaultStepId when
stale. Same single-set call as before; no extra renders.
Confidence: high
Scope-risk: narrow
Not-tested: real backend definition with step IDs that disappear
mid-session (covered by stub + assertion above)
- README test/smoke counts: 18→22, 26→28 (drifted across rounds)
- runLiveFetch keepCurrent now checks the MERGED catalog, not the old
state — fixes the edge case where a scenarioId disappears between
refreshes (would have orphaned the active scenario)
Constraint: no test changes; pure cleanup
Confidence: high
Scope-risk: narrow
Oracle round-2 review caught two real bugs:
1. Production baseUrl bypassed the nginx /api proxy
- api.ts defaulted to https://demo.flow-master.ai in prod
- Browser would hit cross-origin and CORS-fail
- Now: baseUrl='' everywhere; nginx.conf already reverse-proxies /api/*
- vite.config.ts proxy still handles dev
2. Refresh button didn't refresh
- setMode('live') early-returned when already in live mode
- Now: setMode() and refreshLive() share runLiveFetch(); refreshLive
ignores the same-mode guard and always re-runs
- 4 new vitest regressions in state/store.test.ts cover the contract
- Smoke now asserts /api/ea2/work-items is called twice after Refresh
Also:
- buildScenarios.ts parallelized: cap N=6 candidates, Promise.all per-
candidate fetches → live mode now ~3s instead of 30s
- CommandBar + LeftRail preview toasts now name the exact endpoint
(/api/runtime/transactions/{id}/actions) in the visible text
- Landing 'Go live' button rebound to refreshLive() when already live;
copy changed to 'Live · refresh'
- README: scenario table now renders (added separator row); deploy
section points at the real ops PR + the actual overlay path
(overlays/demo, not overlays/mc.flow-master.ai); CORS doc clarifies
same-origin requirement
Constraint: browsers reject cross-origin → same-origin /api/* required
Rejected: dev/prod baseUrl divergence | created production bug
Confidence: high
Scope-risk: narrow
Not-tested: production image actually built + served by ops PR (gated by trusted updater + DNS)
Two-stage build (node:22 → nginx:1.27) bakes dist/ into a static image.
nginx reverse-proxies /api/* to demo.flow-master.ai so live mode works
same-origin without CORS. CI runs vitest + build, then publishes
gitea.flow-master.ai/shad/mission-control-demo:sha-${git} on push to main.
Constraint: backend rejects cross-origin → same-origin proxy required
Confidence: high
Scope-risk: narrow
Not-tested: image actually built in Gitea Actions (requires registry secret)
Polished command-center for FlowMaster with two data modes:
- SNAPSHOT: bundled src/scenarios.json from demo.flow-master.ai
- LIVE: in-browser fetch via src/lib/api.ts (dev-login + bearer)
Scenarios:
- procurement, extra-1, extra-2 (live from EA2)
- ar, hcm, gl, service (industry blueprints, same typed shell)
Honesty pass after Oracle review:
- No invented numbers (Telemetry derives SLA + agent acceptance from real data)
- Preview-only actions fire toasts naming the endpoint to wire them
- Blueprint tours framed as 'industry blueprint', not 'we don't have this yet'
- Mode pill + last-fetch age + refresh in topbar
- Dev CORS dodged via vite proxy; production deploys same-origin
18 vitest tests + 26 playwright smoke assertions + DOM layout audit.
Constraint: cross-origin live mode rejected by browser → fall back to snapshot
Rejected: hardcoded SLA % | dishonest demo metrics
Directive: wire preview-only action handlers to /api/runtime/transactions/{id}/actions to ship them for real
Confidence: high
Scope-risk: narrow
Not-tested: production deployment via flowmaster-ops overlay