CompanyOS — TypeScript / Vite / Reaktor Migration Plan

Public migration plan. This page documents the CompanyOS migration direction that is safe to publish: the TypeScript/Vite cutover, Reaktor decomposition, graph/service adoption, browser automation shape, performance harnesses, and phase gates. Private operational details remain in the local-only private runbooks.

Target: targets/companyOS (companyos-bestbuds) → company.bestbuds.ai. Authoritative code-side doc: bestbuds/AGENTS.md (the repo guide). If this plan and the code disagree, the code wins — verify before acting. Compiled: 2026-06-05. Updated: 2026-06-07. Status: TS/Vite, the typed CompanyOS service contract, Browser Run-backed portal session plumbing, and the Cloudflare Workflow DAG runner are now landed in targets/companyOS. The CompanyOS Playwright suite and data-hygiene checks are the migration's acceptance gate (§9).


Contents

  1. Situation & Goals
  2. Baseline Architecture & Domain Shape
  3. Target Stack — TypeScript + Vite + Cloudflare
  4. Decomposition with Reaktor Principles
  5. Reaktor-Graph + Reaktor-Service Adoption
  6. Browser Automation via Cloudflare Browser Rendering
  7. Automation Recipes
  8. Performance — Lighthouse Profiling & Acceleration
  9. Tests — Using & Refining Codex's Suites
  10. Migration Phases
  11. Risk Matrix
  12. Open Decisions
  13. Appendix — File / Action Map

1. Situation & Goals

CompanyOS is the BestBuds Ventures portal-orchestrator / back-office cockpit: company pulse, finance & runway, people/payroll, cap table & equity, contracts, compliance DAGs, a portal control room, Browser Run session control, and an evidence/audit trail. It is the single-tenant operating surface for the Compliance OS product thesis.

Baseline snapshot (2026-06-05)

This was the starting point that the migration plan was written against. The 2026-06-07 implementation snapshot below records the parts now landed.

AreaBaseline size / shape
DeliveryBuild-free static SPA: public/ served verbatim; no bundler, no node_modules, no build step
RuntimeReact 18.3.1 UMD + @babel/standalone compiling JSX in the browser on every load, all from unpkg.com CDN
Source~8.7k lines: 20 .jsx screens/shell/store + 6 .js data/auth + 1 Worker
StateA single useReducer "god store" (store.jsx, 308 lines, ~25 action types)
Datawindow.MANNA global, assembled from data*.js (incl. data_archive.js, 2,836 lines inline)
RoutingNone — state.screen string + a SCREENS map; the site is a single / route
TypesNone (plain JS/JSX)
AuthCloudflare Worker BFF (src/index.js, 526 lines): Google OIDC → reaktor-auth token exchange, HTTP-only cookie session, server-side refresh, tenant + RBAC gate
TestsNew Codex Playwright suite at tests/companyOS/playwright/ (see §9)

Current implementation snapshot (2026-06-07)

AreaLanded shape
Deliverycompanyos-bestbuds now builds through TypeScript + Vite with worker/index.ts and dist/client assets.
Service contractsrc/core/service/companyOSService.ts defines stable CompanyOS.* operations for graph queries, portal sessions, and workflow runs; the Worker routes through the shared Reaktor service router.
Browser RunPortalSession is a Durable Object backed by the BROWSER binding; it keeps the Cloudflare browser session id, reconnects with Playwright connect(), streams screenshots over WebSocket, and explicitly closes the browser on session close.
Workflow DAGssrc/core/workflow/dagPlanner.ts validates DAGs, rejects cycles/missing edges, topologically orders steps, and classifies sensitive nodes as human gates; worker/dag-workflow.ts executes the plan through Cloudflare Workflows.
Runtime APICompanyOS.Workflow.Plan, Start, Get, and SignalGate are mounted under /api/workflows/*, with a local-plan fallback when the Workflow binding is absent.
Local runtimeHeaded local Playwright session with CDP take-control: scripts/portal-session-headed.mjs opens a real Chromium (operator logs in), scripts/portal-session-drive.mjs attaches via connectOverCDP and drives to the next gate; shared hand-back machine scripts/lib/session-machine.mjs. Verified end-to-end (attach → auto → gate-suspend → resume).
GST adapterscripts/portal-adapters/gst.mjs — login autofill (real selectors) + GSTR-3B step list with encoded human gates (data / approval / dsc); .env/Keychain secret resolver scripts/lib/secrets.mjs (GST_USERNAME/GST_PASSWORD, git-ignored).
TestsThe CompanyOS Playwright suite covers the shared service contract, Worker routing, Browser Run wiring, Workflow binding, DAG planning, and human-gate safety.

Goals (this plan)

  1. TypeScript + Vite — kill in-browser Babel + CDN React; real bundling, types, code-splitting (§3).
  2. Decompose heavily with Reaktor principles — atomic design, typed ports, actors, small composable APIs; dissolve the god-store (§4). Same move we made on the reaktor engine and other modules.
  3. Adopt reaktor-graph + reaktor-service and everything else from Reaktor that fits — reaktor-auth, reaktor-cloudflare, reaktor-db, reaktor-performance, reaktor-telemetry, reaktor-work (§5).
  4. Real portal automation on Cloudflare Browser Rendering — replace the session simulation with human-in-the-loop driven browser sessions, behind hard safety gates (§6).
  5. Lighthouse profiling + acceleration via the existing reaktor-performance harness, with CI budgets (§8).

Non-goals / invariants (do not break)

  • The auth contract is frozen. App id 38d1cee3…, tenant 0d72c513…, cookie names (companyos_at/rt/profile), /auth/* routes, SCREEN_PERMISSIONS, role labels, and the companyos:* scope model must stay byte-compatible so scripts/provision-auth.mjs, existing sessions, and the Google OAuth origin keep working.
  • company.bestbuds.ai never goes dark. Every phase ships; the cutover is parallel-scaffold then swap.
  • The human-in-the-loop safety boundary is a product feature, not a gap: CompanyOS never solves CAPTCHAs, never relays/scrapes OTPs, never automates DSC signing; final portal submits require director approval; cookies are discarded on close; no plaintext secrets/OTPs/PINs are stored or logged. These invariants survive the migration and become tests (§9).

2. Baseline Architecture & Domain Shape

Baseline file map (targets/companyOS, pre-cutover)

src/index.js          Worker BFF: /auth/config|login|session|logout, ensureSession (refresh),
                      authorize() tenant+RBAC gate, summarizeSession→allowedScreens, run_worker_first
public/
  index.html          loads React/ReactDOM/Babel (CDN) + data*.js + every *.jsx via <script type=text/babel>
  auth.js             window.CompanyAuth: /auth/session adapter, canScreen/firstScreen/allowedScreens
  data.js             window.MANNA base contract + fallback fixtures (PEOPLE, COMPANY, PORTALS, RUNS,
                      DAGS, TASKS, DOCS, PACKS, CREDS, COMPLIANCE_REGISTER, STATUS_*, AUDIT)
  data_company.js     company operations slices · data_dags.js · data_archive.js (2,836 lines, generated)
  ui.jsx              shared primitives (Icon, Badge, Avatar, Empty, WarnBox, kv-grids…)
  store.jsx           reducer + StoreContext + initState (the god store)
  shell.jsx           TopBar / Sidebar (rail) / StatusBar
  screens_*.jsx       15 screens (pulse, people, finance, equity, legal, calendar, privacy, command,
                      reconstruction, registry, portals, sessions, dags, ops, system)
  modals.jsx · app.jsx (App root + ReactDOM.createRoot)
scripts/
  import-evidence-archive.mjs   regenerates public/data_archive.js + archive assets from the evidence archive
  provision-auth.mjs            idempotent CompanyOS tenant + RBAC provisioning against reaktor-auth

The data is already a graph

window.MANNA is, in effect, a domain graph that maps cleanly onto reaktor-graph + Memgraph:

  • Entities: Company, Person (founders + external CA/CS/Auditor/Accountant + system agent), Portal (12: MCA, GST, ITD, TRACES, KPT, e-Karmika, Udyam, NSWS, Zoho, Razorpay, Bank, DigiLocker), Run (compliance runs), Dag → DagNode/DagEdge, Task, Doc, EvidencePack, Credential (login hints only — no secrets), ComplianceRow, StatusCandidate, Source, AuditEvent.
  • Edges already exist: Run.portalIds → Portal, Run.dagId → Dag, DagNode --edge--> DagNode, Task.runId/portalId, Doc.runId, Pack.runId, Candidate.required[], audit attribution.
  • Node taxonomy already exists: DAG nodes carry kind (extract_data, reconcile, professional_review, director_approval, captcha_challenge, otp_challenge, portal_prefill, api_submit, evidence_capture, dsc_signing, payment, schedule_next…) and an auto mode (automatic / agent_assisted / human_in_loop / manual_only). This is exactly the actor/human-gate vocabulary required for §6.

The store is a hand-rolled actor

store.jsx's sessionAdvance is a state machine that auto-advances through automatic steps and suspends at human gates (captcha/otp/approval/capture); OrchestrationPanel drives it with a setTimeout auto-advance when there's no challenge. That is precisely a Reaktor ActorNode (mailbox, receive, schedule, supervised suspension) wearing a reducer costume. The migration recognizes this rather than inventing it.

Auth (frozen contract)

src/index.js runs before every asset (run_worker_first: true): exchanges the Google ID token with reaktor-auth (/auth/sign-in), stores access/refresh in HTTP-only cookies, refreshes server-side (/auth/session/refresh), verifies principalKind=USER, appId, tid==tenant, and companyos:superadmin || companyos:admin || companyos:access, and computes allowedScreens from SCREEN_PERMISSIONS. This is a de-facto reaktor-auth client hand-rolled in JS — §5 formalizes it onto AuthService.

3. Target Stack — TypeScript + Vite + Cloudflare

Mirror the in-repo precedents: Manna (manna/targets/appWeb: React 18.3.1 + Vite 7 + TS + Hono Worker + D1) and bestbuds appWeb / reaktorWeb (@cloudflare/vite-plugin, tsc -b && vite build, wrangler static assets + run_worker_first).

ConcernTodayTarget
Compile@babel/standalone in-browservite build (esbuild/rollup), tsc -b for types
ReactUMD dev build from CDNbundled (18.3.1 to match tests; 19 is an open decision)
Moduleswindow.* globals + <script> tagsES modules, typed imports
Workersrc/index.jsworker/index.ts (byte-compatible port; Hono optional later)
Assetspublic/ served verbatimdist/client/ (hashed, immutable, code-split)
Datadata*.js → window.MANNAtyped src/data/*.ts modules; importer emits typed TS
Configwrangler.jsonsame, with assets.directory: dist/client, scoped run_worker_first

Worker port (landed first, lowest risk). The migration translates src/index.js → worker/index.ts while preserving cookie names, route shapes, status codes, and the authorize()/summarizeSession() logic. It adds types for Env, the session summary, and the reaktor-auth responses. Keep not_found_handling: single-page-application. Scope run_worker_first to /auth/* + protected entry routes (an array, like appWeb) instead of true for every asset — this is both an architecture and a perf win (§8).

Frontend. public/*.jsx → src/**/*.tsx; index.html becomes a Vite entry that imports the app (no CDN, no type=text/babel). data*.js → src/data/*.ts typed modules; scripts/import-evidence-archive.mjs emits a typed src/data/companyos-evidence.generated.ts instead of a window.MANNA patch. Route-based code-splitting (React.lazy per screen) so only the landing screen (Pulse) is in the initial chunk and the generated evidence data loads behind the evidence/sources screens.

Build/deploy (package.json): dev: vite (+ wrangler dev for the Worker, or @cloudflare/vite-plugin), build: tsc -b && vite build, deploy: npm run build && wrangler deploy, cf-typegen: wrangler types, plus test:companyos → the Playwright suite.

4. Decomposition with Reaktor Principles

Apply the bar (AGENTS.md §2): expressivity & composability ≫ simplicity ⋙ one-shot/bloated APIs; React + the actor model are the gold standards; atomic design everywhere; a reader understands what any layer orchestrates in one glance. Dissolve the god-store into orthogonal, composable layers:

UI atoms/molecules/organisms (typed, from ui.jsx)         ← pure, prop-driven
  └ screen views (15)                                     ← compose organisms
      └ screen models / view-state                        ← per-screen reactive state (StateFlow-style)
          └ interactors (workflow orchestration)          ← reconstruction, evidence, register, tasks
          └ actors (runtime workflows)                    ← live portal sessions (the sessionAdvance SM)
              └ repositories (per domain, read/write-through, offline-first)
                  └ services (typed contracts) ─────────► graph gateway / HTTP / Browser Rendering

Domain model layer. One typed model per window.MANNA slice (§2). These become the serialization contracts shared by UI, services, and the graph.

Repositories (project the reaktor-db ObjectStore/RepositoryNode pattern into TS, or back them with services in Phase 3): PortalRepository, RunRepository, DagRepository, TaskRepository, DocRepository, EvidenceRepository, CredentialRepository, RegisterRepository, ReconstructionRepository, AuditRepository. Read-through/write-through; offline-first cache; each owns one slice. The ~25 reducer actions move to the repository/interactor that owns their data (see appendix).

Interactors (graph-owned, port-wired) for cross-entity workflows: StatusReconstructionInteractor (importSource/runExtraction/verifyCandidate/generateMissingEvidenceTasks), EvidenceInteractor (captureEvidence, pack completeness), TaskInteractor, RegisterInteractor.

Actors for runtime workflows: PortalSessionActor (the sessionAdvance state machine) — mailbox of step commands, auto-advance via schedule, suspend at human gates, emit tasks/audit. This is the seam that §6 turns into a real Durable Object driving a real browser.

Typed ports for cross-cuts (provider/consumer, à la reaktor-graph-port): AuthPort, TelemetryPort, GraphGatewayPort, PortalAutomationPort. No singletons, no window.*.

Navigation. Replace state.screen + SCREENS map with a real route table (one route per screen) gated by allowedScreens. Deep-linkable, RBAC-aware, keyboard shortcuts preserved. (reaktor-graph RouteNode/RouteBinding if we go Tier-A in §5; otherwise a thin TS router mirroring it.)

Atomic UI. ui.jsx primitives → typed atoms (Icon, Badge, Avatar, Empty, WarnBox, Mono, kv-grid, stepper…); screens compose molecules → organisms; views read from screen models, never from window.MANNA. One-glance readability is the acceptance criterion.

5. Reaktor-Graph + Reaktor-Service Adoption

The honest seam. reaktor-graph and the service layer are Kotlin/KMP with a mature Kotlin/JS target (@JsExport, reaktor-graph/ts/export/*.mjs + .d.mts; reaktor-graph-migration-plan documents WebHost, StateFlow.toReactState(), ReactNode<State>, WebBottomNavigationContainer; Nexergy already consumes reaktor-graph: file:.../reaktor-graph/ts). reaktor-service does not exist as a module yet — the Service/Request/Response/RequestHandler/ServiceNode vocabulary lives in reaktor-graph/.../service and is being extracted (see Service Layer). We adopt in two tiers.

Tier B (do first): service contracts + graph-backed persistence

Define typed Service contracts following the reaktor-service model — operation identity (Compliance.Run.List) distinct from HTTP route, serializable Request/Response, Get/Post handlers, client proxy + server handler from one declaration:

  • PortalService, RunService, DagService, EvidenceService, TaskService, ReconstructionService, RegisterService, CredentialService (read-only login-hints), AuditService.

Browser holds the client handlers; the Worker / reaktorServer holds the server handlers. Persistence:

  • Memgraph via the reaktorServer /_graph gateway (GraphGateway in reaktor-cloudflare: read/write/queryId, tenant-scoped Cypher, JWT worker:server scope, server-side named-query registry). The compliance DAGs, portals, runs, evidence and audit are a graph — this is their natural store. Closest precedent: targets/dataProbe (/_data/graph/*) and Manna's KG access.
  • ClickHouse (:8123) for the audit/analytics event stream (proven live from an edge Worker per reaktor-db).
  • State / relational — D1 or Supabase via the single hyperdrive binding (open decision).

Formalize auth onto reaktor-auth's AuthService / AuthServiceClient instead of the hand-rolled reaktorPost calls in src/index.js — same wire contract, typed, with ServiceInterceptors for tenant/trace propagation. This is the de-facto state made explicit; the §2 invariant (byte-compatible cookies/routes) holds.

Tier A (incremental): in-browser graph runtime

Once Tier B is stable, model CompanyOS itself as a Graph consuming the JS export — RouteNodes per screen, a ContainerNode for the shell, ServiceNode clients for each service, PortalSessionActor as an ActorNode, and StateFlow.toReactState() bridging into the React views. This is the same path reaktorWeb took (see Graph Migration). Adopt per-screen, not big-bang; if the JS export's bundle cost is unacceptable for a back-office app, keep the model (graph/ports/actors as typed TS that mirrors the Kotlin shape) and defer the runtime — captured as a decision, validated with a spike in Phase 3.

Everything-from-Reaktor map

ModuleCompanyOS use
reaktor-authAuthService/AuthServiceClient for the Worker BFF; RBAC scopes (already de-facto)
reaktor-servicetyped service contracts (dev.shibasis.reaktor.service)
reaktor-graphgraph runtime (Tier A), ActorNode for sessions, ServiceNode, ports
reaktor-dbObjectStore/RepositoryNode/offline-first pattern for the repositories
reaktor-cloudflareCloudflareContext, typed Binding<T>, Hono adapter, GraphGateway, Browser Rendering host (§6)
reaktor-performanceLighthouse harness + installReaktorWebVitals() + budgets (§8)
reaktor-telemetrygraph-aware OTel spans on service calls + session steps
reaktor-workProposed server-owned durable scheduling for schedule_next DAG nodes, with occurrence identity and recovery receipts. The current local TaskManager facade alone does not establish next-period compliance execution. See the Work roadmap.

6. Browser Automation via Cloudflare Browser Rendering

Reality: the original "Live Browser Sessions" + "Portal Control Room" screens were simulations (MockPortalPage, setTimeout auto-advance, no backend). The TypeScript target now has the first production seam: a PortalSession Durable Object with a BROWSER binding, Playwright launch/reconnect, browser-session persistence, screenshot streaming, and explicit close semantics. It still needs portal-specific adapters and evidence writes before it should be treated as production filing automation.

Architecture.

  • Cloudflare Browser Rendering (Playwright-driven browser sessions through a browser binding where supported, or the Browser Rendering REST API / external Playwright worker for longer sessions) driven from a Durable Object — one DO per live session. The DO is the PortalSessionActor from §4: it owns the browser session, the step pointer, the human-gate suspension (DO hibernation keeps the session alive across a gate), and emits tasks/audit/evidence.
  • Per-portal adapters keyed by portal.automationMode (Capability/Adapter pattern):
    • human_in_loop_browser (MCA, GST, ITD, TRACES, KPT, e-Karmika, NSWS): drive navigation + server-side credential autofill from the vault up to the human gate, then suspend and stream a live view (periodic page.screenshot() / CDP) to the operator, who solves CAPTCHA/OTP via a real "Take control" handoff. Credentials are injected server-side only — never sent to the browser client.
    • official_api (Zoho, Razorpay, DigiLocker): no browser — use the official API.
    • manual_only (Udyam, Bank): no automation — guided checklist + evidence upload only.
  • Evidence capture (ARN / filed PDF / portal screenshot) → R2, hash-stamped into the evidence graph.

Hard safety gates (encoded, not optional). The adapter state machine must suspend — and can only be advanced by an explicit authenticated human action — at every captcha_challenge, otp_challenge, director_approval, and dsc_signing node. CAPTCHA images are never captured/processed; OTP values are never stored/logged; DSC is signatory-controlled; final submit requires a recorded director approval; cookies are discarded on close. These become the §9 safety tests.

Phasing. Implement one portal end-to-end first — GST GSTR-3B NIL (the existing demo flow) — as the reference automation, then generalize via the adapter registry. Replace the simulation screens with the real session stream behind the same UI.

Limits / decisions. Browser Rendering session-duration and concurrency limits vs. long human-in-the-loop waits may push some portals to an external long-lived browser (browserless) instead of the Workers binding — validate with a GST spike (open decision).

6a. Dual-runtime execution — local Playwright ↔ Cloudflare DO

Updates master-brief §IV.3. That section named GCP Cloud Run as the default Playwright runtime with a desktop worker "later". The decided direction is now two runtimes and no Cloud Run: local Playwright on trusted operator machines (our Macs/PCs) and Cloudflare Browser Rendering Durable Objects for everything else. This matches the JVM-native / no-Node-sidecar cloud plane and keeps government logins on the operator's own IP where possible.

The portable atom is the PortalAdapter; only the browser under it changes. Everything above the adapter (compliance domain, DAG planner, session machine, human gates, service contract) is runtime-agnostic. This works because @cloudflare/playwright is API-compatible with @playwright/test — an adapter written against the Playwright Page (page.goto, page.fill, page.getByRole) runs unchanged locally and at the edge. The one seam that differs is a BrowserSession:

interface BrowserSession {
  page(): Promise<Page>;                 // Playwright-compatible page (local OR @cloudflare/playwright)
  liveView(sink: FrameSink): Promise<void>;
  attach(): Promise<Page>;               // re-attach to an already-open (logged-in) session
  close(): Promise<void>;
}
Local (LocalPlaywrightSession)Edge (CloudflareBrowserSession)
WhereOperator's Mac/PC (CLI / reaktor desktop)Cloudflare Worker + PortalSession DO
Browser@playwright/test chromium.launch()@cloudflare/playwright on BROWSER binding
Agent/automation attachCDP (--remote-debugging-port → connectOverCDP)DO browserSessionId reconnect
Live viewCDP / MJPEG on localhostWebSocket JPEG frames into the web app
SecretsmacOS Keychain / git-ignored .envCloudflare Secrets Store binding
Session persistenceprocess / CDP lifetimeDO storage (browserSessionId)
Gate resumeoperator in window + CLI go / desktop UIDO hibernate + waitForEvent, resolved via app

Runtime resolution (explicit override wins; auto prefers local on trusted devices):

function resolveRuntime(env, config): 'local' | 'edge' {
  if (config.runtime && config.runtime !== 'auto') return config.runtime;
  if (env.BROWSER) return 'edge';                          // running inside the deployed Worker
  if (canLaunchLocalChromium() && isTrustedOperatorDevice()) return 'local'; // Keychain device key from reaktor-auth
  return 'edge';                                           // anything else → managed edge
}

Take-control (the "automation resumes after the human gate" mechanism). Because the operator logs in inside the same browser the automation must drive, the local runtime exposes a CDP port on the headed session and the driver attaches to it:

  1. npm run session:headed -- --portal gst — headed Chromium opens on a CDP port; the operator does the login gate (username/password, CAPTCHA, OTP). No credentials touched by the automation unless the operator opts into vault autofill (never an agent).
  2. npm run session:drive -- --portal gst --fy 2026-27 --period May [--nil] — the driver connectOverCDPs to the logged-in session and takes control, auto-advancing through nav/prefill and suspending at every gate (data, approval, dsc), then resuming after each is cleared.

The edge twin is the existing PortalSession DO (advanceMachine + waitForEvent); Phase 4 unifies both onto one shared SessionMachine + BrowserSession so local and edge are the same code with a different session factory.

6b. Agent-driven operation — Claude / Codex

The master brief's agent model (§II.8, §II.19 allow/forbidden tool lists, §II.18 policy engine) is an internal copilot. This extends it so external coding agents (Claude Code, Codex, scheduled cloud agents) are first-class CompanyOS operators driving the same control surface as the human app — via an MCP server (companyos-automation).

What agents can do: obligation.list / obligation.plan (reason over the obligation graph), session.open / session.drive / session.get_state (drive to the next gate on the resolved runtime), session.inspect (read live DOM), evidence.read, reconciliation.read.

What agents structurally cannot do — the boundary is the tool registry, not a prompt. The MCP server does not register any gate-crossing tool: there is no gate.resolve, solve_captcha, enter_otp, authorize_payment, click_submit, or reveal_secret. This is §II.19's forbiddenTools + §II.18's policy engine, now enforced at the MCP boundary — a jailbroken prompt changes nothing because the capability isn't wired. When session.drive returns a gate, the agent's only move is to notify the responsible human (push / task) and wait.

Self-healing selectors (agent-native robustness). Government DOMs drift. On auto_failed{selector}, the agent calls session.inspect, reads the DOM, and proposes a patched locator via adapter.suggestSelector(stepId, locator); human-approved patches are written back to the adapter. This turns selector rot from an outage into a self-correcting loop — the single biggest win of agent integration and the fix for the "post-login GST selectors need one live pass" gap.

Attribution. Every agent action carries { agentId, onBehalfOf: humanId }; the audit log distinguishes agent-driven auto steps from human-cleared gates. An agent with no human principal may read/plan but cannot open a session that will reach a gate.

7. Automation Recipes

These recipes are concrete operating patterns for routine CompanyOS work. Each recipe should be represented as a typed DAG, planned through CompanyOS.Workflow.Plan, started through CompanyOS.Workflow.Start, and paused through explicit gate events when legal, security, or portal controls require a person.

The common execution pattern is:

  • Automatic steps run as Cloudflare Workflow step.do() items: graph reads, official API calls, reconciliation, evidence hashing, task creation, and safe browser navigation.
  • Browser steps run through PortalSession: launch/reconnect Browser Run, navigate, prefill non-sensitive fields, stream a live view, and capture allowed evidence.
  • Human gates use step.waitForEvent() and CompanyOS.Workflow.SignalGate: CAPTCHA, OTP, DSC signing, payment authorization, professional review, director approval, and final submit.
  • Evidence lands in the evidence graph and R2 with hashes, source URLs, actor ids, and timestamps. Secrets, OTPs, CAPTCHA text, DSC PINs, and cookie values never enter logs, payloads, audit rows, or storage.
#RoutineTriggerDAG shapeGuardrails / output
1GST GSTR-3B NIL filingMonthly compliance calendar, no outward/inward tax dataLoad GST run → reconcile books → Browser Run login/prefill → CAPTCHA/OTP gate → director approval → submit → capture ARN/PDF → schedule next monthNo CAPTCHA/OTP persistence; ARN, filing PDF, portal screenshot, and audit event are recorded.
2GST GSTR-1 invoice summaryInvoice cutoff or monthly closePull invoices from Zoho/Razorpay → classify B2B/B2C/export/credit notes → reconcile with books → GST Browser Run prefill → professional review → human submit gateDiff report plus prepared return; final portal submission stays human-controlled.
3GST cash-ledger and challan paymentTax payable or low cash-ledger balanceCompute liability → open GST challan flow → prefill amount/head → payment gate → capture CIN/challan → update registerPayment is always a human gate; only challan metadata and receipt evidence are stored.
4TDS monthly challanPayroll/vendor-payment closeClassify payments → compute sections/rates → professional review → ITD/TRACES Browser Run → payment gate → capture challan/CINProfessional review required before payment; output links deductions to challan evidence.
5Quarterly TDS return packQuarter endAggregate challans, deductees, PAN validation, and corrections → generate return pack → CA review gate → filing handoff → acknowledgement captureCompanyOS prepares the pack and evidence; professional filing stays gated.
6TDS Form 16 / 16A reconciliationReturn accepted or certificate window opensTRACES Browser Run → CAPTCHA gate → download certificates → match employees/vendors → flag mismatches → archive certificatesCertificates are stored as evidence; portal challenges remain human-only.
7MCA AOC-4 / MGT-7 annual filing packFinancial-year close and AGM calendarBuild statutory checklist → collect financials/board docs → CS/CA review → MCA Browser Run → DSC/director gate → SRN receipt captureDSC is never automated; output is SRN, signed form evidence, and compliance-register update.
8MCA master-data and charge-status watcherWeekly or before funding diligenceBrowser/API status check → compare CIN, directors, registered office, charges, and filing status → create exceptions → archive current snapshotCAPTCHA gates pause the run; output is a diff against the CompanyOS graph.
9Board meeting pack and minutesCalendar event or compliance deadlineBuild agenda → attach financial/compliance inputs → circulate draft → approval gate → finalize minutes → assign follow-up tasksNo portal automation required; creates a board evidence pack and task graph.
10Director DIR-3 KYC readinessAnnual KYC window or director changeValidate director details → gather proof docs → MCA Browser Run → director OTP/DSC gate → SRN capture → next-year reminderDirector-controlled OTP/DSC only; CompanyOS records readiness and receipt evidence.
11Shops and Establishment renewalLicense-expiry lead timeLoad jurisdiction checklist → Browser Run or guided manual portal → fee/payment gate → upload renewal certificate → update compliance registerState portals vary; unsupported portals degrade to checklist plus evidence upload.
12Professional Tax PTEC/PTRC paymentMonthly/annual due date by stateCompute liability → state portal Browser Run → payment gate → challan capture → register updatePayment requires explicit approval; challan and ledger entries are linked.
13PF/ESI registration readinessEmployee-count or payroll-policy thresholdMonitor employee count → collect employee/company docs → professional review → create portal onboarding tasks → archive decisionNo auto-registration until applicability is confirmed by a professional.
14Udyam/MSME certificate refreshQuarterly status check or profile changeGuided checklist → portal status screenshot/manual upload → field diff → evidence pack update → reminder schedulingmanual_only mode by default; CompanyOS assists with reminders and evidence only.
15DPIIT / Startup India status trackerWeekly application monitor or funding milestonePortal/API status check → detect query/defect → create owner task → draft response pack → archive status evidenceSubmissions and declarations stay approval-gated; status evidence is safe to automate.
16Bank statement import and reconciliationDaily/weekly finance closeSecure file/API import → parse transactions → reconcile payroll/vendor/tax payments → exception tasks → runway dashboard refreshBank credentials are never exposed to the client; unmatched transactions become tasks.
17Vendor bill ingestion and TDS/GST classificationNew invoice upload/emailExtract invoice fields → classify GST/TDS treatment → match PO/contract → professional review if uncertain → create payment/challan tasksLow-confidence tax classification is a review gate; invoice evidence is hash-stamped.
18Payroll run evidence packMonthly payroll closeCompute payroll summary → approval gate → payment checklist → payslip/document archive → TDS/PF/ESI task fan-outSalary payment and statutory remittance are human gates; evidence links payroll to downstream filings.
19Contract renewal and notice workflowRenewal/termination lead timeParse contract metadata → owner review → draft notice/task → approval gate → archive signed notice/amendment → schedule next checkpointLegal notices require owner approval; CompanyOS keeps reminders and evidence consistent.
20Evidence-pack completeness and audit freezeBefore any filing, diligence, or board packQuery required evidence graph → detect missing docs → create tasks → freeze pack → director/professional sign-off → R2 archive manifestProduces a hash-stamped manifest; incomplete packs cannot be marked ready without an explicit override.

Implementation hooks:

  • src/core/workflow/dagPlanner.ts is the shared planner: it topologically sorts, rejects invalid graphs, and converts sensitive steps into explicit gates.
  • worker/dag-workflow.ts is the durable executor: automatic nodes run, human-gate nodes hibernate until the matching event arrives, and mismatched gate signals fail the run.
  • worker/portal-session.ts owns Browser Run lifecycle: launch, reconnect by browserSessionId, stream, and close.
  • src/data/services/workflow.ts is the browser-facing client used by the DAG inspector and future recipe launchers.
  • tests/companyOS/playwright/companyos-workflow.spec.ts is the regression guard for planning, binding configuration, Browser Run persistence, and human-gate safety.

8. Performance — Lighthouse Profiling & Acceleration

Use the existing reaktor-performance harness — do not hand-roll. It ships a Lighthouse envelope (ReaktorPerformanceHarness), installReaktorWebVitals(), ReaktorPerformanceReport, and budgetViolations() / requireWithinBudgets().

Baseline first (current SPA). The build-free model is a known, large perf cost: 3 render-blocking CDN scripts, @babel/standalone shipped and run on every load to compile ~20 JSX files in the browser, data_archive.js (2,836 lines) inline, and run_worker_first: true taxing every asset request with a Worker round-trip. Capture Lighthouse + Core Web Vitals (LCP/INP/CLS/FCP/TBT) on company.bestbuds.ai before migrating — that's the number we beat.

The Vite migration is itself the biggest win: removes runtime Babel, tree-shakes, minifies, content-hashes for immutable caching, and code-splits per route. Then targeted passes:

LeverAction
Runtime compileDelete @babel/standalone + CDN React → bundled production React
Code splittingReact.lazy per screen; initial chunk = shell + Pulse only
Deferred datadata_archive / generated evidence data load behind evidence/sources screens
Worker taxScope run_worker_first to /auth/* + protected routes (not all assets)
CachingHashed immutable assets at the edge; R2 for evidence/PDF with cache headers
Fontspreconnect + display=swap already present — keep; self-host if it helps LCP

Budgets (CI gate). LCP < 2.5s · INP < 200ms · CLS < 0.1 · TBT < 200ms · initial JS per route under budget. Wire Lighthouse into CI via the Dagger webChecks surface + the perf harness (precedent: perf:reaktorWeb), failing the build on budgetViolations(). Perf is continuous, not a one-time phase.

9. Tests — Using & Refining Codex's Suites

What exists (tests/companyOS/playwright/, Codex-owned, against the current SPA):

  • helpers/companyOS.ts — boots by mocking the unpkg.com CDN scripts (react, react-dom, @babel/standalone) from local node_modules, mocking /auth/session with a superadmin fixture, and probing window.CompanyAuth / window.MANNA. Exports the canonical 21-row screens list, bannedDataPatterns, collectClientErrors, openScreen.
  • companyos-flows.spec.ts — opens every screen asserting no console/page errors, then exercises sub-flows (People→Hiring/Payroll, Equity→ESOP/Fundraising, Privacy, Sessions→"Mark CAPTCHA solved", Source Library→Ask, all Settings sections).
  • companyos-data-integrity.spec.ts — asserts the runtime window.MANNA + COMPANYOS_EVIDENCE_ARCHIVE and the served public/*.{html,js,jsx,css} contain no legacy source residue or known fake fixtures.

Migration impact (what breaks, and the refinement):

CouplingBreaks becauseRefinement
installCompanyOSRoutes mocks unpkg.com react/babelVite serves a real bundle; no CDNRetarget the boot helper at the Vite dev server / preview build; drop the CDN mock; keep the /auth/session superadmin mock
window.MANNA / window.CompanyAuth probesbecomes typed modules, not globalsExpose a dev/test-only typed debug handle (e.g. window.__companyos) or move assertions to data-testid / role selectors; data-integrity scans the typed data module + built dist/ output
.rail .nav-item / .screen / .brand-nameDOM may change in decompositionPreserve these hooks (or testids) through §4 so the smoke stays stable

Refinement plan (coordinate with Codex — they own writes now):

  1. Keep the behavioral coverage as the phase gate. "Open every screen, no client errors" + the sub-flows are the cross-cutting regression guard; they must pass green at every phase. Port assertions from DOM-coupled → route/contract/actor-level as we decompose, but never lose the end-to-end smoke.
  2. Add route + RBAC tests — deep-link each screen; assert allowedScreens gating (a scoped CA/CS session sees only its screens).
  3. Add actor/session state-machine unit tests — the PortalSessionActor must not auto-advance through captcha/otp/approval/dsc without an explicit human command; auto-advance only across automatic steps.
  4. Add service-contract tests — Request/Response round-trip per service; transport-status vs logical-status semantics (per reaktor-service).
  5. Harden the safety scan — extend bannedDataPatterns to assert no OTP / CAPTCHA / DSC PIN / cookie value ever persists in any store, service payload, audit row, or R2 object (§6 invariant as an automated check).

10. Migration Phases

Each phase ships; company.bestbuds.ai stays up; the (adapted) Playwright suite stays green.

As of 2026-06-07, the TS/Vite cutover and the first Workflow/Browser Run slice are landed: the Worker has a typed service contract, the portal-session Durable Object owns Browser Run lifecycle, COMPANYOS_DAG_WORKFLOW is configured, and the Playwright suite verifies the shared contract and safety gates. The remaining work is portal-specific adapters, evidence persistence, graph-backed storage, and performance hardening.

PhaseGoalExit criteria
0 — Baseline & scaffoldLighthouse baseline on prod. Stand up Vite+TS skeleton alongside the SPA. Port src/index.js → worker/index.ts (byte-compatible auth). Get Codex's suite running against current + new.Baseline captured; Worker port passes the /auth/* integration tests; suite green
1 — TS + Vite cutover (req 1)JSX→TSX, bundle React, delete Babel/CDN, typed data modules, route code-splitting. Adapt the Playwright harness (drop CDN mock).Lighthouse re-baselined with quantified win; suite green; prod swapped to the bundled build
2 — Decomposition (req 2)God-store → domain models + repositories + interactors + atomic UI; real routes + RBAC gating; PortalSessionActor extracted. Still local/typed data.One-glance layering; route/RBAC + actor tests added & green
3 — Graph + Service (req 3)Tier-B service contracts + persistence (Memgraph /_graph, ClickHouse audit, D1/Hyperdrive state); AuthService formalized; Tier-A graph-runtime spike.Services back the repositories; contract tests green; auth contract unchanged
4 — Browser automation (req 4)Browser Rendering + DO session actors; GST end-to-end with hard safety gates; evidence→R2; per-portal adapters; simulation screens replaced. Unify local + edge onto one shared SessionMachine + BrowserSession with resolveRuntime() (§6a); verify GST post-login selectors against a live login.GST NIL flow real on both runtimes, human gates enforced by tests; safety scan green
4a — Agent bridge (§6b)Ship the companyos-automation MCP server (plan + drive-to-gate + inspect), gate/secret tools structurally excluded; self-healing selectors; agent attribution + audit.Claude/Codex can drive a GST run to each gate and no further; excluded tools provably absent
5 — Perf hardening (req 5)reaktor-performance Lighthouse harness + budgets in CI (Dagger); final acceleration pass; lock budgets.Budgets enforced in CI; regressions fail the build

11. Risk Matrix

RiskImpactMitigation
Auth contract driftLocks out the team / breaks provisioningByte-compatible Worker port; /auth/session integration test; freeze cookie names/routes/scopes (§2)
company.bestbuds.ai downtimeBack-office darkParallel scaffold; phased swap; each phase shippable + reversible
reaktor-graph JS-export bundle costHeavy initial load for a back-office appValidate Tier-A early with a spike; fall back to typed-TS-mirrors-the-model, defer runtime
Browser Rendering limits vs long human waitsSessions time out at a gateDO hibernation; evaluate external browser for long sessions; GST spike decides
Automating a human gateLegal/ethical breachHard-coded suspension + explicit-human-command-only advance, enforced by tests (§9.3/9.5)
Memgraph gateway unreachable from edgeData screens emptyStatic snapshot fallback (Manna precedent); health-gated UI
Codex test coupling churnSuite thrash during decompositionCoordinate ownership; preserve smoke hooks; port assertions level-by-level
Dirty worktree on reaktorWeb deployShips unrelated in-flight changesBuild first; report what ships; deploy reaktorWeb only (not the whole repo)

12. Open Decisions

  1. React 18 vs 19 — 18.3.1 matches the current CDN + Codex's mocks (Manna/appWeb use 18.3.1); reaktorWeb is on 19. Lean: 18.3.1 through Phase 1, revisit at Phase 3.
  2. @cloudflare/vite-plugin vs manual vite + wrangler dev proxy — appWeb/reaktorWeb use the plugin; Nexergy uses a manual /api proxy. Lean: the plugin (closest precedent).
  3. Tier-A in-browser graph runtime vs Tier-B services-only — does the browser run reaktor-graph, or stay plain typed React consuming services while the graph lives server-side? Lean: Tier-B first, Tier-A incrementally per-screen after a spike.
  4. Persistence split — Memgraph (/_graph) + ClickHouse (audit) + D1 vs Supabase Hyperdrive for state. Lean: graph in Memgraph, audit in ClickHouse, state in D1; reconcile with reaktor-db.
  5. Browser Rendering binding vs external browser for long human-in-the-loop sessions — decided by the GST spike.
  6. reaktor-service is extracted — import typed service contracts from :reaktor-service (dev.shibasis.reaktor.service); see the reaktor-service doc.
  7. Runtime detection (§6a) — auto-probe (canLaunchLocalChromium + Keychain device key) vs an explicit per-invocation flag. Lean: explicit COMPANYOS_RUNTIME override, auto default that prefers local on trusted devices. Supersedes master-brief §IV.3's Cloud Run default.
  8. Agent integration channel (§6b) — MCP server (portable across Claude Code / Codex) vs a Reaktor Agent SDK binding. Lean: MCP first (both speak it), SDK later for richer typing.
  9. Local control plane (§6a) — CLI only, or the reaktor desktop app hosting the local Playwright driver so operators get the live view + gate UI natively. Lean: desktop app as the operator cockpit; CLI for scripting/CI. (Aligns with master-brief §IV.1's "desktop worker later" for DSC-heavy flows.)

13. Appendix — File / Action Map

Current file → target module

CurrentTarget
src/index.jsworker/index.ts → later AuthService server handlers (reaktor-auth)
public/auth.jssrc/auth/AuthClient.ts (AuthServiceClient) + typed Session
public/data*.jssrc/data/*.ts typed models; generated evidence-data module from the importer
public/store.jsxdissolved → repositories + interactors + PortalSessionActor + screen models
public/ui.jsxsrc/ui/atoms/*.tsx
public/shell.jsxsrc/ui/shell/{TopBar,Sidebar,StatusBar}.tsx
public/screens_*.jsxsrc/screens/*/{View,model}.tsx behind routes
public/modals.jsx · app.jsxsrc/ui/ModalHost.tsx · src/App.tsx (router root)
scripts/import-evidence-archive.mjskeep; emit typed TS instead of window.MANNA patch

window.MANNA slice → model · service · graph

SliceModelServiceGraph node (Tier A)
PORTALSPortalPortalServicePortal node + automationMode adapter
RUNSRunRunServiceRun node → Portal/Dag edges
DAGSDag/DagNode/DagEdgeDagServicethe compliance graph (Memgraph)
TASKSTaskTaskServiceTask node
DOCS / PACKSDoc / EvidencePackEvidenceServiceevidence subgraph + R2
CREDSCredential (hints only)CredentialService (RO)Credential node (no secrets)
COMPLIANCE_REGISTERComplianceRowRegisterServiceregister projection
STATUS_CANDIDATES/SOURCESStatusCandidate/SourceReconstructionServicereconstruction subgraph
AUDITAuditEventAuditServiceappend-only → ClickHouse

Reducer action → new home (samples)

ActionHome
nav / selectPortal / selectSessionrouter + RBAC guard
openSession / sessionAdvance / captureEvidencePortalSessionActor (→ DO + Browser Rendering, §6)
importSource / runExtraction / verifyCandidate / generateMissingEvidenceTasksStatusReconstructionInteractor
uploadDoc / approveSubmitEvidenceInteractor / director-approval gate
createTask / completeTaskTaskInteractor / TaskRepository
updateComplianceRegisterRegisterInteractor
logAuditRepository (→ ClickHouse)

This is a migration record and next-step plan. Current state, proposed model, phases, tests, and open decisions are kept separate above; verify each surface against the code before acting on it.

  • Service Layer - Use before adding service APIs, splitting reaktor-service out of reaktor-graph, wiring handlers to Spring/Workers, or making modules graph-projectable without hard graph dependencies.
  • Work - Use before changing background scheduling, worker contracts, WorkRequest/WorkConstraints, BestBuds workers, edge/web scheduling, work telemetry, or graph projection for work.
  • React x Compose - Use before touching the React/Compose bridge.
  • Perf and Observability - Use before optimizing or adding instrumentation.
  • Shadow Dev - Use before building production-like test and replay environments.
  • UI & Design System - Use before touching reaktor-ui tokens/components/theming, designing the tactile runtime (interaction/physics/field/skins/host-tree/reconciler), or planning adaptive layouts and AI/server-driven UI.