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
- Situation & Goals
- Baseline Architecture & Domain Shape
- Target Stack — TypeScript + Vite + Cloudflare
- Decomposition with Reaktor Principles
- Reaktor-Graph + Reaktor-Service Adoption
- Browser Automation via Cloudflare Browser Rendering
- Automation Recipes
- Performance — Lighthouse Profiling & Acceleration
- Tests — Using & Refining Codex's Suites
- Migration Phases
- Risk Matrix
- Open Decisions
- 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.
| Area | Baseline size / shape |
|---|---|
| Delivery | Build-free static SPA: public/ served verbatim; no bundler, no node_modules, no build step |
| Runtime | React 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 |
| State | A single useReducer "god store" (store.jsx, 308 lines, ~25 action types) |
| Data | window.MANNA global, assembled from data*.js (incl. data_archive.js, 2,836 lines inline) |
| Routing | None — state.screen string + a SCREENS map; the site is a single / route |
| Types | None (plain JS/JSX) |
| Auth | Cloudflare Worker BFF (src/index.js, 526 lines): Google OIDC → reaktor-auth token exchange, HTTP-only cookie session, server-side refresh, tenant + RBAC gate |
| Tests | New Codex Playwright suite at tests/companyOS/playwright/ (see §9) |
Current implementation snapshot (2026-06-07)
| Area | Landed shape |
|---|---|
| Delivery | companyos-bestbuds now builds through TypeScript + Vite with worker/index.ts and dist/client assets. |
| Service contract | src/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 Run | PortalSession 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 DAGs | src/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 API | CompanyOS.Workflow.Plan, Start, Get, and SignalGate are mounted under /api/workflows/*, with a local-plan fallback when the Workflow binding is absent. |
| Local runtime | Headed 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 adapter | scripts/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). |
| Tests | The CompanyOS Playwright suite covers the shared service contract, Worker routing, Browser Run wiring, Workflow binding, DAG planning, and human-gate safety. |
Goals (this plan)
- TypeScript + Vite — kill in-browser Babel + CDN React; real bundling, types, code-splitting (§3).
- 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.
- Adopt
reaktor-graph+reaktor-serviceand everything else from Reaktor that fits —reaktor-auth,reaktor-cloudflare,reaktor-db,reaktor-performance,reaktor-telemetry,reaktor-work(§5). - Real portal automation on Cloudflare Browser Rendering — replace the session simulation with human-in-the-loop driven browser sessions, behind hard safety gates (§6).
- Lighthouse profiling + acceleration via the existing
reaktor-performanceharness, with CI budgets (§8).
Non-goals / invariants (do not break)
- The auth contract is frozen. App id
38d1cee3…, tenant0d72c513…, cookie names (companyos_at/rt/profile),/auth/*routes,SCREEN_PERMISSIONS, role labels, and thecompanyos:*scope model must stay byte-compatible soscripts/provision-auth.mjs, existing sessions, and the Google OAuth origin keep working. company.bestbuds.ainever 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 anautomode (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).
| Concern | Today | Target |
|---|---|---|
| Compile | @babel/standalone in-browser | vite build (esbuild/rollup), tsc -b for types |
| React | UMD dev build from CDN | bundled (18.3.1 to match tests; 19 is an open decision) |
| Modules | window.* globals + <script> tags | ES modules, typed imports |
| Worker | src/index.js | worker/index.ts (byte-compatible port; Hono optional later) |
| Assets | public/ served verbatim | dist/client/ (hashed, immutable, code-split) |
| Data | data*.js → window.MANNA | typed src/data/*.ts modules; importer emits typed TS |
| Config | wrangler.json | same, 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/_graphgateway (GraphGatewayinreaktor-cloudflare:read/write/queryId, tenant-scoped Cypher, JWTworker:serverscope, 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 perreaktor-db). - State / relational — D1 or Supabase via the single
hyperdrivebinding (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
| Module | CompanyOS use |
|---|---|
reaktor-auth | AuthService/AuthServiceClient for the Worker BFF; RBAC scopes (already de-facto) |
reaktor-service | typed service contracts (dev.shibasis.reaktor.service) |
reaktor-graph | graph runtime (Tier A), ActorNode for sessions, ServiceNode, ports |
reaktor-db | ObjectStore/RepositoryNode/offline-first pattern for the repositories |
reaktor-cloudflare | CloudflareContext, typed Binding<T>, Hono adapter, GraphGateway, Browser Rendering host (§6) |
reaktor-performance | Lighthouse harness + installReaktorWebVitals() + budgets (§8) |
reaktor-telemetry | graph-aware OTel spans on service calls + session steps |
reaktor-work | Proposed 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
browserbinding 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 thePortalSessionActorfrom §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 (periodicpage.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) | |
|---|---|---|
| Where | Operator's Mac/PC (CLI / reaktor desktop) | Cloudflare Worker + PortalSession DO |
| Browser | @playwright/test chromium.launch() | @cloudflare/playwright on BROWSER binding |
| Agent/automation attach | CDP (--remote-debugging-port → connectOverCDP) | DO browserSessionId reconnect |
| Live view | CDP / MJPEG on localhost | WebSocket JPEG frames into the web app |
| Secrets | macOS Keychain / git-ignored .env | Cloudflare Secrets Store binding |
| Session persistence | process / CDP lifetime | DO storage (browserSessionId) |
| Gate resume | operator in window + CLI go / desktop UI | DO 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:
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).npm run session:drive -- --portal gst --fy 2026-27 --period May [--nil]— the driverconnectOverCDPs 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()andCompanyOS.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.
| # | Routine | Trigger | DAG shape | Guardrails / output |
|---|---|---|---|---|
| 1 | GST GSTR-3B NIL filing | Monthly compliance calendar, no outward/inward tax data | Load GST run → reconcile books → Browser Run login/prefill → CAPTCHA/OTP gate → director approval → submit → capture ARN/PDF → schedule next month | No CAPTCHA/OTP persistence; ARN, filing PDF, portal screenshot, and audit event are recorded. |
| 2 | GST GSTR-1 invoice summary | Invoice cutoff or monthly close | Pull invoices from Zoho/Razorpay → classify B2B/B2C/export/credit notes → reconcile with books → GST Browser Run prefill → professional review → human submit gate | Diff report plus prepared return; final portal submission stays human-controlled. |
| 3 | GST cash-ledger and challan payment | Tax payable or low cash-ledger balance | Compute liability → open GST challan flow → prefill amount/head → payment gate → capture CIN/challan → update register | Payment is always a human gate; only challan metadata and receipt evidence are stored. |
| 4 | TDS monthly challan | Payroll/vendor-payment close | Classify payments → compute sections/rates → professional review → ITD/TRACES Browser Run → payment gate → capture challan/CIN | Professional review required before payment; output links deductions to challan evidence. |
| 5 | Quarterly TDS return pack | Quarter end | Aggregate challans, deductees, PAN validation, and corrections → generate return pack → CA review gate → filing handoff → acknowledgement capture | CompanyOS prepares the pack and evidence; professional filing stays gated. |
| 6 | TDS Form 16 / 16A reconciliation | Return accepted or certificate window opens | TRACES Browser Run → CAPTCHA gate → download certificates → match employees/vendors → flag mismatches → archive certificates | Certificates are stored as evidence; portal challenges remain human-only. |
| 7 | MCA AOC-4 / MGT-7 annual filing pack | Financial-year close and AGM calendar | Build statutory checklist → collect financials/board docs → CS/CA review → MCA Browser Run → DSC/director gate → SRN receipt capture | DSC is never automated; output is SRN, signed form evidence, and compliance-register update. |
| 8 | MCA master-data and charge-status watcher | Weekly or before funding diligence | Browser/API status check → compare CIN, directors, registered office, charges, and filing status → create exceptions → archive current snapshot | CAPTCHA gates pause the run; output is a diff against the CompanyOS graph. |
| 9 | Board meeting pack and minutes | Calendar event or compliance deadline | Build agenda → attach financial/compliance inputs → circulate draft → approval gate → finalize minutes → assign follow-up tasks | No portal automation required; creates a board evidence pack and task graph. |
| 10 | Director DIR-3 KYC readiness | Annual KYC window or director change | Validate director details → gather proof docs → MCA Browser Run → director OTP/DSC gate → SRN capture → next-year reminder | Director-controlled OTP/DSC only; CompanyOS records readiness and receipt evidence. |
| 11 | Shops and Establishment renewal | License-expiry lead time | Load jurisdiction checklist → Browser Run or guided manual portal → fee/payment gate → upload renewal certificate → update compliance register | State portals vary; unsupported portals degrade to checklist plus evidence upload. |
| 12 | Professional Tax PTEC/PTRC payment | Monthly/annual due date by state | Compute liability → state portal Browser Run → payment gate → challan capture → register update | Payment requires explicit approval; challan and ledger entries are linked. |
| 13 | PF/ESI registration readiness | Employee-count or payroll-policy threshold | Monitor employee count → collect employee/company docs → professional review → create portal onboarding tasks → archive decision | No auto-registration until applicability is confirmed by a professional. |
| 14 | Udyam/MSME certificate refresh | Quarterly status check or profile change | Guided checklist → portal status screenshot/manual upload → field diff → evidence pack update → reminder scheduling | manual_only mode by default; CompanyOS assists with reminders and evidence only. |
| 15 | DPIIT / Startup India status tracker | Weekly application monitor or funding milestone | Portal/API status check → detect query/defect → create owner task → draft response pack → archive status evidence | Submissions and declarations stay approval-gated; status evidence is safe to automate. |
| 16 | Bank statement import and reconciliation | Daily/weekly finance close | Secure file/API import → parse transactions → reconcile payroll/vendor/tax payments → exception tasks → runway dashboard refresh | Bank credentials are never exposed to the client; unmatched transactions become tasks. |
| 17 | Vendor bill ingestion and TDS/GST classification | New invoice upload/email | Extract invoice fields → classify GST/TDS treatment → match PO/contract → professional review if uncertain → create payment/challan tasks | Low-confidence tax classification is a review gate; invoice evidence is hash-stamped. |
| 18 | Payroll run evidence pack | Monthly payroll close | Compute payroll summary → approval gate → payment checklist → payslip/document archive → TDS/PF/ESI task fan-out | Salary payment and statutory remittance are human gates; evidence links payroll to downstream filings. |
| 19 | Contract renewal and notice workflow | Renewal/termination lead time | Parse contract metadata → owner review → draft notice/task → approval gate → archive signed notice/amendment → schedule next checkpoint | Legal notices require owner approval; CompanyOS keeps reminders and evidence consistent. |
| 20 | Evidence-pack completeness and audit freeze | Before any filing, diligence, or board pack | Query required evidence graph → detect missing docs → create tasks → freeze pack → director/professional sign-off → R2 archive manifest | Produces a hash-stamped manifest; incomplete packs cannot be marked ready without an explicit override. |
Implementation hooks:
src/core/workflow/dagPlanner.tsis the shared planner: it topologically sorts, rejects invalid graphs, and converts sensitive steps into explicit gates.worker/dag-workflow.tsis 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.tsowns Browser Run lifecycle: launch, reconnect bybrowserSessionId, stream, and close.src/data/services/workflow.tsis the browser-facing client used by the DAG inspector and future recipe launchers.tests/companyOS/playwright/companyos-workflow.spec.tsis 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:
| Lever | Action |
|---|---|
| Runtime compile | Delete @babel/standalone + CDN React → bundled production React |
| Code splitting | React.lazy per screen; initial chunk = shell + Pulse only |
| Deferred data | data_archive / generated evidence data load behind evidence/sources screens |
| Worker tax | Scope run_worker_first to /auth/* + protected routes (not all assets) |
| Caching | Hashed immutable assets at the edge; R2 for evidence/PDF with cache headers |
| Fonts | preconnect + 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 theunpkg.comCDN scripts (react,react-dom,@babel/standalone) from localnode_modules, mocking/auth/sessionwith a superadmin fixture, and probingwindow.CompanyAuth/window.MANNA. Exports the canonical 21-rowscreenslist,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 runtimewindow.MANNA+COMPANYOS_EVIDENCE_ARCHIVEand the servedpublic/*.{html,js,jsx,css}contain no legacy source residue or known fake fixtures.
Migration impact (what breaks, and the refinement):
| Coupling | Breaks because | Refinement |
|---|---|---|
installCompanyOSRoutes mocks unpkg.com react/babel | Vite serves a real bundle; no CDN | Retarget the boot helper at the Vite dev server / preview build; drop the CDN mock; keep the /auth/session superadmin mock |
window.MANNA / window.CompanyAuth probes | becomes typed modules, not globals | Expose 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-name | DOM may change in decomposition | Preserve these hooks (or testids) through §4 so the smoke stays stable |
Refinement plan (coordinate with Codex — they own writes now):
- 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.
- Add route + RBAC tests — deep-link each screen; assert
allowedScreensgating (a scoped CA/CS session sees only its screens). - Add actor/session state-machine unit tests — the
PortalSessionActormust not auto-advance throughcaptcha/otp/approval/dscwithout an explicit human command; auto-advance only acrossautomaticsteps. - Add service-contract tests —
Request/Responseround-trip per service; transport-status vs logical-status semantics (per reaktor-service). - Harden the safety scan — extend
bannedDataPatternsto 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.
| Phase | Goal | Exit criteria |
|---|---|---|
| 0 — Baseline & scaffold | Lighthouse 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
| Risk | Impact | Mitigation |
|---|---|---|
| Auth contract drift | Locks out the team / breaks provisioning | Byte-compatible Worker port; /auth/session integration test; freeze cookie names/routes/scopes (§2) |
company.bestbuds.ai downtime | Back-office dark | Parallel scaffold; phased swap; each phase shippable + reversible |
reaktor-graph JS-export bundle cost | Heavy initial load for a back-office app | Validate Tier-A early with a spike; fall back to typed-TS-mirrors-the-model, defer runtime |
| Browser Rendering limits vs long human waits | Sessions time out at a gate | DO hibernation; evaluate external browser for long sessions; GST spike decides |
| Automating a human gate | Legal/ethical breach | Hard-coded suspension + explicit-human-command-only advance, enforced by tests (§9.3/9.5) |
| Memgraph gateway unreachable from edge | Data screens empty | Static snapshot fallback (Manna precedent); health-gated UI |
| Codex test coupling churn | Suite thrash during decomposition | Coordinate ownership; preserve smoke hooks; port assertions level-by-level |
Dirty worktree on reaktorWeb deploy | Ships unrelated in-flight changes | Build first; report what ships; deploy reaktorWeb only (not the whole repo) |
12. Open Decisions
- 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.
@cloudflare/vite-pluginvs manualvite+wrangler devproxy — appWeb/reaktorWeb use the plugin; Nexergy uses a manual/apiproxy. Lean: the plugin (closest precedent).- 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. - Persistence split — Memgraph (
/_graph) + ClickHouse (audit) + D1 vs Supabase Hyperdrive for state. Lean: graph in Memgraph, audit in ClickHouse, state in D1; reconcile withreaktor-db. - Browser Rendering binding vs external browser for long human-in-the-loop sessions — decided by the GST spike.
reaktor-serviceis extracted — import typed service contracts from:reaktor-service(dev.shibasis.reaktor.service); see the reaktor-service doc.- Runtime detection (§6a) — auto-probe (
canLaunchLocalChromium+ Keychain device key) vs an explicit per-invocation flag. Lean: explicitCOMPANYOS_RUNTIMEoverride,autodefault that prefers local on trusted devices. Supersedes master-brief §IV.3's Cloud Run default. - 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.
- 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
| Current | Target |
|---|---|
src/index.js | worker/index.ts → later AuthService server handlers (reaktor-auth) |
public/auth.js | src/auth/AuthClient.ts (AuthServiceClient) + typed Session |
public/data*.js | src/data/*.ts typed models; generated evidence-data module from the importer |
public/store.jsx | dissolved → repositories + interactors + PortalSessionActor + screen models |
public/ui.jsx | src/ui/atoms/*.tsx |
public/shell.jsx | src/ui/shell/{TopBar,Sidebar,StatusBar}.tsx |
public/screens_*.jsx | src/screens/*/{View,model}.tsx behind routes |
public/modals.jsx · app.jsx | src/ui/ModalHost.tsx · src/App.tsx (router root) |
scripts/import-evidence-archive.mjs | keep; emit typed TS instead of window.MANNA patch |
window.MANNA slice → model · service · graph
| Slice | Model | Service | Graph node (Tier A) |
|---|---|---|---|
| PORTALS | Portal | PortalService | Portal node + automationMode adapter |
| RUNS | Run | RunService | Run node → Portal/Dag edges |
| DAGS | Dag/DagNode/DagEdge | DagService | the compliance graph (Memgraph) |
| TASKS | Task | TaskService | Task node |
| DOCS / PACKS | Doc / EvidencePack | EvidenceService | evidence subgraph + R2 |
| CREDS | Credential (hints only) | CredentialService (RO) | Credential node (no secrets) |
| COMPLIANCE_REGISTER | ComplianceRow | RegisterService | register projection |
| STATUS_CANDIDATES/SOURCES | StatusCandidate/Source | ReconstructionService | reconstruction subgraph |
| AUDIT | AuditEvent | AuditService | append-only → ClickHouse |
Reducer action → new home (samples)
| Action | Home |
|---|---|
nav / selectPortal / selectSession | router + RBAC guard |
openSession / sessionAdvance / captureEvidence | PortalSessionActor (→ DO + Browser Rendering, §6) |
importSource / runExtraction / verifyCandidate / generateMissingEvidenceTasks | StatusReconstructionInteractor |
uploadDoc / approveSubmit | EvidenceInteractor / director-approval gate |
createTask / completeTask | TaskInteractor / TaskRepository |
updateComplianceRegister | RegisterInteractor |
log | AuditRepository (→ 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.
Related docs
- 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.