Ventari mark VENTARI

active working standard · Ventari Team

Client CRM Factory.

The operating standard for building, launching, and improving every custom client CRM instance

Estate v1.6 Factory v0.11 active working standard Updated 7 October 2026 Phases 1–14

The simple story

Build once.
Fit each client.

The CRM Factory is one safe system for many different businesses. We keep the strong parts the same, then change the brand, tools, and workflow for each client.

How one CRM becomes many.

Every client starts with the same proven core. Their pack tells the core what to show, how to look, and which tools to connect.

01
Learn

Map the business

We learn the people, tools, data, and work.

02
Build

Make the client pack

The pack holds the brand, modules, and rules.

03
Launch

Install and check

We connect the database, test it, and release it.

04
Improve

Share better parts

A safe core update can help every client.

What made it possible
Estate MapKnow what exists
Shared coreOne proven CRM
Client packsFit each business
Release proofCheck before launch

One engine. Many different fits.

The core stays stable. A small client pack changes what people see and use. Client data remains in that client’s own database.

Pick a business to see its CRM

One shared core, a small pack for each client, their own CRM Three inputs (the signed agreement, discovery calls and the tools the client uses) flow into a small client pack file that switches modules on or off. The shared CRM core is identical for every client. The result is that client's own CRM, shown as a working window with its own theme, fonts and navigation, plus its own database and its own safe release. WHAT GOES IN CLIENT PACK SHARED CORE SAFE UPDATES Signed agreementwhat they get Discovery callshow they work Tools they usewhat to connect pack.config.ts One file. One line each. 12of 20 modules on 7 of 8 tools connected CRM coreSame for every client Contacts Pipeline Delivery Reporting Automations Safety Releases Built once. Tested. v34 v35 v36 v37 next No one moves becausesomeone else moved. Owndatabase Each client's data staysin their own database. THEIR OWN CRM
Ventari
Today

Sample data for illustration

+ New

    Step 1 · The client pack

    A small file for each client

    It says which modules and tools this client gets. Nothing else changes.

    Modules switched on12 of 19
    Tools connected7 of 8

    Step 2 · The CRM core

    One shared, tested core

    The same for every client. The pack only decides what is switched on.

    • Contacts
    • Pipeline
    • Delivery
    • Reporting
    • Automations
    • Safety
    • Releases

    Brain and knowledge modules stay dark until a venture pack turns them on.

    Step 3 · Their own CRM

    What the client sees

    Ventari
    • Own database
    • Own release

    Every module and tool option, and what this example switches on

    Modules · 20

      Social reply guard is off in every pack today. Its production adapter has not been built.

      Tools · 10

        Optional tools are available to any pack and are not part of this example.

        Planned modules · 6
        • EducationPlanned
        • CommunityPlanned
        • EventsPlanned
        • MembershipsPlanned
        • InventoryPlanned
        • Field servicePlanned

        Future shapes. They are not in the pack schema yet.

        Options come from the current pack schema and the Connections screen. The on/off states for the example clients are illustrative.

        DataEach client owns their data

        Every client’s records live in their own database.

        ReleaseNo surprise updates

        A client moves only when their release is chosen and checked.

        LearningGood work becomes reusable

        A proven improvement can be shared with every client.

        System indexSearch or browse all 29 sections

        Document 1

        Ventari Estate Map

        17 sections · Phases 1–8 landed · Version 1.6

        Part I · Phases 1–8

        Ventari Estate Map

        The canonical map of Ventari's systems, boundaries, and blast radius

        The canonical map of Ventari's digital estate: what exists, who owns it, and what a change affects. Generated where possible, reviewed where judgment is required, and readable by people and agents.

        Approved 2026-09-07. Phases 1–8 have landed. Version 1.6 records the final state; sections 14–16 hold the shipped work and the change history.


        Ventari Estate Map · Foundation

        Purpose

        This is the complete specification for one canonical map of the Ventari digital estate

        It is not a request for more documentation. Ventari already writes good documentation, and section 3 says so with evidence. It is a request for the one artifact none of that documentation provides: a view of how the parts relate, and what a change to one costs elsewhere.

        The intended readers should use this document to:

        • understand what the map is before agreeing to build any of it;
        • see which parts are genuinely necessary and which are optional, stated plainly;
        • identify the phases and their dependencies;
        • know what "done" means for each phase before it starts;
        • judge whether the map earns its maintenance cost;
        • dispatch a single numbered section to a worker without further interpretation.

        Every top-level and second-level heading is stable and citable. Implement 8.2 is a complete instruction.

        1.1 Evidence labels

        Every substantive claim in this document carries one of four labels. Where a claim carries none, treat it as Design requirement.

        • Confirmed — verified directly against committed code, repository settings, or a generated artifact, on 2026-09-07. The check is named.
        • Documented — Ventari's own documents state it. Recorded as their claim, which is not the same as verified.
        • Inferred — an interpretation. A hypothesis, not a fact, and labelled so it can be argued with.
        • Design requirement — a product decision derived from the above.

        1.2 What this document deliberately does not contain

        • Time estimates. None, for any phase. Sizing before phase 1 would be a guess presented as a plan. Phase 1 exists partly to make estimation possible.
        • Ownership of the resulting artifact. Settled in 12.1 as of 0.2: Cody owns the generated layer, Nico's agents own the written layer.
        • Production truth. Everything here was read from repositories. Ventari's own custody plan records that the live database has drifted from repository migration files, so repository code is not evidence of what production runs. (Documented.)

        Ventari Estate Map · Foundation

        Executive verdict

        Full specification textdetail for agents and careful review

        Ventari does not have a documentation problem. It has a seams problem.

        Every individual area is well described. The calendar control plane, Client Health and the Work Ledger each carry a real map, and the calendar map was updated on 2026-09-07. (Confirmed: docs/calendar-control-plane/CONTEXT.md frontmatter verified: 2026-09-07.)

        Correction, 2026-09-07

        Version 0.1 of this document claimed no cross-repository map existed. That was wrong, and it was the central premise. VentariBrain/_canon/SYSTEMS-AND-FEATURES.md is a canonical cross-repository registry: eight systems with their local paths, repositories, agent entry points and ownership, plus 38 directional Touches lines for features that cross systems. (Confirmed by reading it.) The error came from building this picture from the app side; the Brain had no readable checkout on the authoring machine until after the first draft was written.

        This document now extends that registry rather than proposing a replacement. Region cards cite registry entries and never restate owner, status or verifier. The registry's own update contract remains the write rule for feature facts. The anti-pattern in 11.2 about a second vocabulary applies to this document first.

        What the registry does not yet carry, and what this proposal adds:

        • A universe label. Nothing marks a repository live, leftover or dead, and 95 repositories exist. (Confirmed.)
        • Blast radius as a lookup. Touches is directional and per feature. There is no "if you change X, open these" index across the estate. (Confirmed.)
        • Generated inventories. Routes, tables, functions, policies, scheduled jobs and egress points are not enumerated anywhere. (Confirmed.)

        The consequence is carried by whoever happens to remember. That is affordable for people who have been here since the start. It is not affordable for an agent, which is where this stops being a documentation nicety and starts being infrastructure.

        The map should do four things:

        1. Name every part. One entry per repository, service and server, with a living/finished/dead label.
        2. State each part's boundary. What it owns, what it may write to, what it must never touch.
        3. Answer blast radius. If you change X, open these. This is the part that pays every week.
        4. Stay true without maintenance. Everything that changes often is generated from source. Only boundaries are written by hand.

        Ventari Estate Map · Foundation

        Current estate

        3.1 Maps that exist and work

        (Confirmed.) Three area maps in VentariFullApp, all in current use:

        Thirteen CONTEXT.md files in total. The calendar map describes itself as "a no-move overlay: runtime ownership stays in app/, website/, VentariBrain/, and the private audit engine." That is precisely the additive, non-conflicting principle this specification adopts, already written down and already practised here. It is not a new idea being imported.

        Alongside them: nine ADRs with a decision register and a machine-readable authority-registry.json, a route convergence map and surface inventory for paid client fulfilment, a prompt assembly inventory, and roughly twenty runbooks. (Confirmed.)

        This body of work is the reason the proposal is small. The method is proven inside Ventari, by Ventari. This extends it one level up.

        3.2 ICM is already part of the system

        (Confirmed.) Three ways:

        • The icm-architect skill is installed on the Ventari operating desk.
        • "ICM workspaces" is a first-class component type in the product. PRODUCT.md lists what a Capability may combine, and ICM workspaces are in that list alongside Agents, Skills, repositories and workflows. DESIGN.md repeats it for the Capabilities List.
        • The client Brain agent identifies as an ICM librarian in its own prompt (lib/client-brain/librarian.ts).

        Method lineage: Interpretable Context Methodology, Van Clief and McDermott, arXiv:2603.16021, MIT licensed. (Documented, by the local skill.) Nothing here requires anyone to adopt a new methodology.

        3.3 The estate, counted

        (Confirmed, 2026-09-07, via the GitHub API and committed files.)

        The gap between 95 and 11 is the finding, not the numbers. The generated repository list is accurate about what it measures, which is local clones, and is easily read as the estate.

        3.4 What no map covers

        Corrected 2026-09-07. Cross-repository relationships are covered, by _canon/SYSTEMS-AND-FEATURES.md. See section 2. What remains uncovered:

        (Confirmed.)

        • Whether a repository is live, leftover or dead. Two hosted projects are flagged "stale deploys possible" in a file last verified 2026-08-05.
        • Blast radius as a lookup: Touches is per feature and directional, and no index answers "if you change X, open these" across the estate.
        • Generated inventories of routes, tables, functions, policies, scheduled jobs or egress points.
        • The 84 repositories that are not among the eight systems the registry names.

        3.5 The governance layer this document did not originally account for

        (Confirmed 2026-09-07, after Nico's review.) None of the following is reachable from the code, and version 0.1 was written without it. It binds this work:

        Two decisions from the W0–W16 build bind every region card: rules live in the database rather than in application checks, and each paying client gets a dedicated agent instance. A boundary that disagrees with either is wrong.


        Ventari Estate Map · Foundation

        Agent impact

        (Inferred, but from confirmed structure.)

        The Executive runs across the whole estate: onboarding, builds, money, communication, growth. Every one of those crosses several parts of the system. It currently has no representation of what those parts are.

        1. Orientation cost is paid every run. Without a map, every task begins by rediscovering the ground from source. That cost is paid in tokens and in wrong turns, on every task, forever.
        2. Learning needs names to attach to. The system is designed to convert outcomes into reusable plays. A system cannot learn about parts of itself it cannot name. The map supplies the nouns that outcomes attach to. This is the single strongest argument in this document.
        3. Build agents need blast radius, not just instructions. An agent asked to change one thing should be able to look up what else that touches. Today it cannot, so either a person checks or nobody does.
        4. The reusable client core needs the seam made visible. ADR-005 already decided on a neutral core, an agency pack and tenant configuration. Its two blocking follow-ups remain open and its install fixture is marked not run. (Documented.) The map is what makes the shared/bespoke seam visible enough to act on.

        The test this document holds itself to: the map must let the Executive answer at least one question it cannot answer today. If it cannot, build 6.1 to 6.3 for people and stop.


        Ventari Estate Map · Foundation

        Rules we follow

        1. Point, do not repeat. A card holds shape, boundary and blast radius. Counts and status live in generated files, or in the source that owns them.
        2. Every card is stamped with a date and a revision. verified_at and verified_revision. A card can be out of date and still honest, because it says what it was true against.
        3. The source always wins. When a card and the source it describes disagree, the card is wrong and flips to stale.
        4. The map never moves anything. It sits on top of the estate. Who owns what at run time does not change because the map exists.
        5. Colour is never the only signal. Anything a person has to act on is also written in words.
        6. Every phase ships a generated file or a wired check. No phase ships prose alone. See 11.1 for why this one comes first.
        7. Keep it proportional. A small change gets routing and nothing else. Ceremony costs time, and time is not free.

        Ventari Estate Map · Foundation

        Estate model

        6.1 Node types

        Closed set. Anything that fits none of these does not belong in the map.

        6.2 Universes

        Every region declares one. This is the single most useful label in the map, because the estate contains 95 repositories and nobody knows how many are alive.

        6.3 Frontmatter

        Every card carries type, universe, status (stub / verified / stale), verified_at, and verified_revision. status is the card's freshness only; it never copies or replaces a feature's implementation status in the registry. Region cards also carry entity, the stable canonical region name, plus members, the relevant census ids. The registry and census remain the sources; cards cite them and do not copy owner, feature status, evidence, verifier, or generated counts.

        status: verified requires all three of verified_at, verified_revision, and a Sources section citing what was read. A confident wrong date is worse than stale.

        6.4 What the map deliberately does not hold

        • Product behaviour. The app, the engine and the vault own their own truth. Cards point and stop.
        • Current status. The backlog and lane status move daily. A card restating them would be wrong within a day.
        • Secrets. No card names a credential value, ever.

        Ventari Estate Map · Foundation

        Generated vs written

        The split is by rate of change, not by level of detail. This is the decision that determines whether the map survives its first month.

        Target: the hand-written layer fits on one page. If it grows past that, the map has started documenting instead of mapping.


        Ventari Estate Map · Operating standard

        Phases 1–8

        Each phase names what it produces and one Exit sentence. A phase is not finished until its exit condition is demonstrably true.

        Corrected 2026-09-07

        Version 0.1 presented these as a standalone roadmap. The program's rule (§0a) is that new work enters the master as scoped rows, and five of the six already had program identity. These are program rows, not a parallel track. Progress is reported by VE id in the claim issue, in the program's state vocabulary: discovery, ready, local_verified, review, released, done.

        8.1 Phase 1 — Census

        • Enumerate all 95 repositories, all hosted projects, all servers.
        • Classify each live, leftover or dead.
        • Record last activity, default branch, and whether anything deploys from it.
        • Emit as a generated file, rebuilt by script.

        Exit: every repository, hosted project and server in the estate carries a universe label, and the file that says so can be regenerated by one command.

        8.2 Phase 2 — Regions

        • Write one card per region of the live estate.
        • Each names its boundary: what it owns, what it may write, what it must not touch.
        • One index page listing every region, generated.
        • The Executive read returns the region index only. Blast radius is Phase 3 and joins the read there — see the 12.3 supersession.

        Exit: a person or agent who has never seen the estate can name every live region and its boundary after reading one page.

        8.3 Phase 3 — Blast radius

        • Build the "if you change X, open these" index.
        • Include the "does not hit" column, which prevents false alarm.
        • A validator that fails when a card cites a path that no longer exists.

        Exit: for every region, the map names what a change to it touches, and the validator passes.

        8.4 Phase 4 — Check audit

        Full specification textdetail for agents and careful review

        Amended 2026-09-09. The counts below were measured at 1771e2f and have moved; the exit condition has changed. Both are recorded here rather than silently edited, because this document was approved with the original wording.

        • Classify all 824 verification scripts: load-bearing, duplicate, or stale.
        • Start with the 41 whose names indicate a security concern.
        • Wire the load-bearing ones up. Delete duplicates and stale checks.

        Exit: every verification script has one of three verdicts recorded, and every script the audit calls load-bearing runs without a human choosing to run it. the deletions are merged

        Why deletion left this phase

        Measured 2026-09-09 at app main df62cfa:

        The problem this phase exists to solve is not that 824 files exist. It is the sentence in §9.1: nobody has established which matter, whose consequence is that 34 run and the rest do nothing. Deleting four hundred files makes nothing safer. Wiring up the right sixty does.

        The risks are also not symmetric. Turning a check on is cheap and reversible, and a flaky one announces itself on the next push. Deleting a check is the one that bites, and it bites months later, in the place the check used to be.

        And the obvious signal for "stale" is unavailable here: every one of the 824 was last touched in August or September 2026. There is no dusty corner. Stale has to mean tests something that no longer exists or duplicates another check, both of which cost real work to establish and neither of which is urgent.

        So Phase 4 records a verdict for all 824 and ships the wire-up. Deletion becomes its own later lane, batched, once the wired set has run long enough to show which checks earn their place. Decision 12.2 already routes it: Alex records, Cody accepts or rejects, never the same person.

        What makes a verdict readable by a non-specialist

        A classification nobody can check is an opinion. Two things keep this one honest:

        • The registry already answers part of it. _canon/SYSTEMS-AND-FEATURES.md carries a Verify: field on 36 of 39 features, each naming exact commands. That is Ventari's own written claim about which checks are load-bearing, and no phase has ever used it.
        • Mutation is the test. For any script the audit calls load-bearing, break the thing it guards and confirm the script goes red. A check that stays green while its subject is broken is not load-bearing, whatever its name says. This is Phase 3's lesson applied: grade against evidence that can refute, never against a plausible list.

        8.5 Phase 5 — Gate proofs

        Full specification textdetail for agents and careful review

        Amended 2026-09-09, after A3 failed. The original exit is recorded below struck through rather than deleted, because this document was approved with it.

        • One failing-attempt test per protected action.
        • Two-tenant isolation fixture: read one client's data as another and expect refusal.
        • Run against a local database stack before anything hosted.

        Exit: a measured verdict for A3, A4, A5 and H9 by id, and every check that already proves a gate runs without a human choosing to run it. each gate has a test that attempts the forbidden action and fails correctly

        Why the exit changed: A3 fails

        Run 2026-09-09 against a disposable local Postgres with 276 migrations applied, two synthetic tenants, connected as the authenticated role with RLS in force and is_service_role() = false:

        Staff of synthetic installation B read and wrote installation A's vault and tasks. The writes persisted. Seven leaks across tasks and vault_audit_reports.

        The cause, verified against source rather than inferred:

        CREATE FUNCTION public.is_staff() ...
          SELECT public.has_role('admin') OR public.has_role('team');
        
        CREATE POLICY vault_audit_reports_write ON public.vault_audit_reports
          FOR ALL USING (public.is_service_role() OR public.is_staff())
          WITH CHECK (public.is_service_role() OR public.is_staff());
        

        is_staff() carries no tenant, organization or installation predicate.

        This is not a live breach. There is one tenant, literalTenantId: 'ventari', and staff-global access to Ventari's own data is correct behaviour for single_agency_literal. Client-seat isolation held: client B could not read or write client A's tasks, refused by can_access_organization.

        The fix already exists and is 92% unapplied. foundation_can_access_tenant, in migrations 0065/0068/0069, is:

        SELECT public.is_service_role()
          OR (public.is_staff() AND public.foundation_current_tenant_id() = p_tenant_id::text);
        

        That is the correct predicate. It is adopted on 16 of 226 staff policies and the rollout stopped. And verify-foundation-security-isolation, which tests exactly this with two synthetic tenants, passes today and is orphaned with no npm script, so nothing defends the pattern that works.

        Why Phase 5 does not do the rollout

        Applying the predicate to 210 policies is a schema change to the company's production database. Under 12.2 that is Cody's to accept, and under §11 it is not something a lane that just found the problem should also ship. Phase 5 measures and records; the rollout is its own row.

        Keeping the original exit would have forced one of two dishonest outcomes: weakening a test until it passed, or leaving the phase open for weeks while a fix nobody has approved is written. Neither is worth more than a measured verdict.

        H9 is recorded not proven, not failed. Both tenants were denied on the receipts table, but by a table GRANT rather than an installation predicate, and the own-tenant control did not succeed. A denial that cannot be distinguished from a missing grant is not evidence of isolation.

        What this means for Phase 6

        Corrected 2026-09-09, later the same day. The paragraph below was wrong and is kept struck through because it was acted on.

        Phase 6 is a second installation, and A3 says a second installation would expose both tenants to each other's staff. The rollout is therefore on the critical path to Phase 6.

        The deployment tier was already chosen, and it is not shared-database. docs/architecture/multi-tenant-custody-plan.md, three pages past the section this phase started from:

        1. shared_rls_isolated — one Postgres, many tenant_id values, tenant-aware RLS. Rejected as the cutover strategy.
        2. hybrid — chosen. Ventari remains on the current single-agency stack. Peer agency tenants get a private-stack data plane. Shared artifacts are code, tenant-pack manifests and factory receipts, never client facts. In-agency clients stay on the organization-subtree.

        verify-tenant-isolation asserts tier=hybrid on every run.

        Under hybrid, two peer tenants never share a database. So A3's scenario — staff of B reading A's rows in one Postgres — is not one the chosen architecture creates. The same document says which wall is which:

        "Tenant" here means a peer agency data plane, not a Ventari client organization. Client/org leakage inside Ventari is a separate organization-subtree duty.

        So there are two walls and this phase conflated them:

        A3's failure is real and the measurement stands. It is not the critical path to Phase 6. The 207-policy rollout matters if Ventari ever adopts shared-database multi-tenancy, which the custody plan explicitly rejected.

        How this was missed

        The phase read is_staff(), the policies and the region cards, and stopped before the deployment-tier section of a document it had already opened. One document, read partly, produced a confident conclusion about the business's critical path. That is the same failure this map keeps finding in other people's work.

        The three delivery models, and what actually shipped

        Established 2026-09-09 while answering "where does Rising Origin fit":

        Both client CRMs that shipped are clone-style. Neither used the chosen tier. Two deliveries, two people, the same instinct, while the register treats hybrid as the direction.

        The likely reason is timing rather than disagreement: growth-os was extracted 2026-09-05, after the Rising Origin build. There was no reusable core to reuse when the work happened.

        The real Phase 6 question, and it is open

        Answered 2026-09-09 by Phase 6 stage 1. See §8.6. A private stack works with 'ventari' as its internal tenant id; a distinct id is not equivalent. And the 482 below is the misleading count — it is functions that contain the literal. The number that actually refuses a non-'ventari' tenant is 9 by static scan, 22 at runtime. This subsection is left as written because it records what was true and open when Phase 5 shipped.

        482 distinct database functions contain a 'ventari' literal

        , across 2,260 occurrences. The custody plan records 305; it has grown. (Corrected from 513 on 2026-09-09: the first count split on the next CREATE FUNCTION, so the last function in each file absorbed trailing SQL. 482 counts dollar-quoted bodies only. The 2,260 occurrence figure was right both times and is the robust one.)

        Whether that blocks a peer tenant depends on a design question nobody has answered: does a private stack use 'ventari' as its own internal tenant id, or a distinct one?

        • If each private stack is internally 'ventari', the literals are harmless. The tenant packs point this way: helix.ts declares literalTenantId: 'ventari'.
        • If a private stack carries tenant_helix, then 482 functions return nothing for it, and parameterising them is the real Phase 6 cost.

        That is a founder-level architecture call, and it is the question to put to Nico rather than the isolation rollout. (It was settled by building instead. §8.6.)

        8.6 Phase 6 — The reusable client core

        Full specification textdetail for agents and careful review

        Unblocked and given a name, 2026-09-09. Version 0.8 recorded Phase 6 as blocked by A3. It is not: the chosen tier is hybrid, peer tenants get their own database, and A3 measures a shared-database boundary that tier avoids. §8.5 carries the correction.

        The test case is Rising Origin, and it was always the right one. This phase already said derive contents empirically from the one real client build and build one client from the result. Rising Origin is the one real client build. Naming it is not new scope; it is this section with the blank filled in.

        Why Rising Origin, on the evidence

        It was hand-built in the client's own GitHub organisation with its own 14 migrations, finished before growth-os existed. Its migration names are:

        leads_pipeline · grant_lead_table_privileges · organizations_workspace · attach_lead_to_existing_organization · identity_roots · security_boundary · auth_person_map · transactional_lead_intake · staff_team · identity_lift_backfill · attribution_ledger · staff_access_partial_update · tighten_function_grants · organization_soft_delete

        Corrected 2026-09-09. Version 1.0 listed seven of the fourteen and called them the list. Re-read from rising-crm/supabase/migrations/ at stage 1.

        Its tables include people, organizations, person_organization_membership, notes, staff_access, staff_role_capability, external_identity.

        Every one of those roots already exists in the Ventari app

        in 0001_identity_authorization.sql, role_assignments, and can_access_organization. Identity, org membership, staff roles and security boundaries were built twice. That duplication is the cost this phase exists to remove, and it is measured rather than argued.

        Two stages. Only the first is Phase 6

        Stage 1 — build it dry. Stand up a Rising-Origin-shaped instance from the Ventari codebase plus a tenant-pack manifest, against a fresh local database. Never the client's. Then compare it to what they actually run.

        It answers three things, in a day or two, with zero client exposure:

        • Does the 'ventari' literal problem bite? 482 functions carry one. This is the fastest way to find out and it decides everything downstream.
        • What does Rising Origin have that the core lacks? leads, lead_stage_history, lead_attribution_event and client_audits are theirs. Are they in the core, or would "turning on a feature" mean building it first?
        • What does "turn on" actually mean — a manifest flag, a migration, or a feature that does not exist yet?

        Stage 2 — the migration itself. Its own row, gated on stage 1 coming back clean and on the client agreeing. Not part of this phase.

        Why the stages are split

        Rising Origin is a live system a paying client depends on. Every phase before this one was additive or measurement. This would be the first time the estate map touches something in use, which is a different risk class, and the rule that has held all week applies harder: prove it locally, then propose.

        The commercial half is a founder's call, not an engineering one

        The offer — the client keeps their database, Ventari owns the codebase, and features get switched on rather than rebuilt — is decision D19 stated as a product: "what a client owns: data, database and instance yes; source code no." It is a conversation to have with them, not an assumption to build on, and it is far stronger after stage 1 because it shows a working instance instead of describing one.

        Exit: a Rising-Origin-shaped client instance has been built from the reusable core against a local database, the gap list is recorded, and the install fixture is proven to contain no client facts, secrets or prices.

        Stage 1 answered all three, 2026-09-09

        Run in the lane at sessions/estate-map-client-core-2026-09-09/, against a disposable local Postgres with 276 migrations applied. Landed as VentariFullApp 9eefb89b (#536), which also closes the six-row program: VE-178 71b3e766, VE-179 95871851, VE-180 89707dbe, VE-181 1333ef9, VE-182 3cfbd69, VE-183 stage 1 9eefb89b. Nothing touched Rising Origin's live system at any point. Worker 02 was sealed from worker 01.

        1. Does the 'ventari' literal problem bite? No, provided every stack keeps the literal. A private-stack instance works with 'ventari' as its internal tenant id. A distinct tenant id is not equivalent: it is legal on the domain and writable onto rows, and then the governed CRM RPCs refuse it.

        482 is the misleading number and it is the one this document quoted. Counts at 3cfbd69, method beside each:

        pipeline_lifecycle_transition is in the small set: 0097 line 163 raises pipeline_lifecycle_tenant_invalid. So Phase 6 is configuration, not a parameterisation project. A single-digit gate is a config decision; 482 functions would have been a rewrite, and quoting 482 unqualified would have priced this phase wrong.

        2. What does Rising Origin have that the core lacks? Two things, not four. Of 16 tables: none drop in unchanged, 12 re-implement a core noun under a different name or shape, 2 are genuine builds, and 2 should not be ported at all.

        • leads and lead_stage_history are opportunities and opportunity_stage_history. Confirmed rather than assumed: their own migration header at 20260823185621_leads_pipeline.sql lines 4-7 says it was modelled on 0003_crm_pipelines_import.sql. There is no CREATE TABLE public.leads anywhere in the 276 core migrations. What they need is a 7-row pipeline seed, not a table.
        • lead_attribution_event is a genuine build. Core's attribution_touches and attribution_decisions are affiliate ownership of an email hash and require an affiliate_id; Rising Origin's is lead source categorisation.
        • client_audits is a product call: build a thin CRM card, or point that pane at the existing audits / audit_runs brand-audit engine.
        • The stub people / legacy_people_lift should not be ported. It exists because Rising Origin built identity twice.

        3. What does "turn on" mean? A pack, not a flag. lib/tenant-packs/helix.ts declares no crm module; ventari-agency.ts does. A Rising-Origin shape is a new pack or a trimmed agency pack.

        Exit met, all three conditions

        One caveat worth stating rather than burying: the install fixture check proves the detector, by planting a client fact, a secret and a price on every run and failing if any class goes uncaught. ADR-005's "publish versioned synthetic install fixture" is still open, and a gap is a finding in this lane. The check is the 68th in core-stability, so it runs without anyone choosing to run it.

        One finding, recorded not fixed

        record_opportunity_stage_change never sets tenant_id, so stage history on a synthetic-tenant opportunity defaulted to 'ventari'. Harmless while every stack is internally 'ventari', which is the recorded answer above. It is exactly what would silently corrupt data the day anyone tries a distinct id. In docs/estate/FINDINGS.md with a Cody decision line.

        Stage 2 is unchanged and still not this phase.

        Stage 1.5 — the dry migration, added 2026-09-09

        Stage 1 built a database, not an application, and not one row has ever been mapped. That is the correct outcome for the question stage 1 asked, and it leaves a gap between "we know what it costs" and "we can propose it to a client".

        Stage 1.5 closes that gap without going near the client. Their 14 migrations give us their exact schema, so synthetic data can be generated in their shape, the real mapping run against it, and the result stood up as something to look at. Their own data stays where it is until they have said yes.

        The measure is row counts in versus out, per table, with every discrepancy explained. Not "it ran" and not "no errors". A migration that loses 3% of leads demos perfectly. A mapping that drops rows is a finding, and finding one is this stage succeeding.

        The design that makes the result credible is the same one that worked in stages 3 to 6: the worker generating the data is sealed from the worker writing the mapping. Otherwise the data gets shaped to fit the mapping and the run passes for the wrong reason.

        Exit: a mapping specification covering all 16 tables, a synthetic dataset carrying the awkward cases their schema permits, a run with per-table counts and every difference named, a preview an owner can click, and a written answer on the reusable attribution contract.

        Attribution is a named exit condition, not a footnote

        (Alex, 2026-09-09: if Ventari builds CRMs for clients, lead attribution should be one elegant thing that copies into each new CRM rather than being rebuilt per client.) The core already has three evidence layers — touches (0003 line 243), the self-reported acquisition source contract, and the observed first-touch / last-eligible-non-direct contract — two of them defended by running checks. It has no adjudication ledger for lead source. Rising Origin's lead_attribution_event is exactly that: pending / attributed / excluded / disputed, with written evidence and a named actor required for every decision. Whether that belongs in the core, in a pack, or with the client is a design question this stage answers in writing and does not build.

        Correction recorded against the stage 1 gap map: it classified lead_attribution_event as a genuine build having weighed 0005 only, never mentioning touches or the booking contracts. The conclusion holds. The reasoning was incomplete, and acting on it would have meant rebuilding evidence capture the core already has.

        Lane: sessions/estate-map-dry-migration-2026-09-09/.

        What stage 1.5 measured, 2026-09-09

        Landed as VentariFullApp 1c141acb (#540). Nothing touched Rising Origin's live system and no real client row entered the lane at any point.

        The data moves. The product does not yet present it as a client CRM. Those are two different answers and stage 1 only ever tested the first.

        The migration is real, on a synthetic dataset built from their schema by a worker sealed from the mapping:

        Both unique indexes were confirmed present by definition before any refusal was read as meaningful, so the single dropped row (two people sharing a phone number) is a genuine constraint rather than a silently skipped index.

        The first run failed, and that is why the lane exists

        A lead with a null person link minted a person from its denormalised contact fields, fabricating a second copy of a human who had already migrated. Not a drop and not a corruption: a third class, a row that should not exist. The rule became match before minting, with ambiguity routed to identity_review_cases rather than merged, which is the pattern the core already uses for shared inboxes. The re-run linked that lead to the existing person instead of cloning them.

        The application layer is where the parameterisation work actually is. Four findings, each with a file and a line, all in docs/estate/FINDINGS.md:

        1. The funnel board is hardcoded to ventari_sales. A Rising-Origin user opens fifteen Ventari columns, all empty, while their 31 leads sit on a pipeline the board never selects. A pack export does not change a constant.
        2. The CRM workspace reads organization_memberships; the mapping writes person_organization_relationships. An organisation with contacts attached renders "No people linked". The core carries two nouns for one idea and the interface reads the other one.
        3. The pack cannot load without registration. REGISTERED_PACKS holds only Helix and the agency pack, and host bindings only bind to registered packs.
        4. Fresh client stacks receive Ventari's own seeded organisations. A private stack built from these migrations ships with other clients' names inside it.

        That fourth one deserves its own sentence. verify-install-fixture-cleanliness shipped in stage 1 precisely to stop client facts reaching an install artifact, and it works. Seed migrations are a different artifact and nothing checks them. The guard is real and pointed at a different door.

        So the stage 1 conclusion stands and needed a qualifier. Configuration rather than a parameterisation project was true of the database. It was not true of the application, and this document said it without that qualifier.

        Exit met. Mapping specification, sealed synthetic dataset, per-table counts with every difference named, a preview, and the written attribution contract. The preview's verdict is that it is not fit to show a client, which is a result rather than a failure: it cost synthetic rows instead of a call.

        verify:tenant-packs became the 69th core-stability check in the same merge. It was already orphaned before this lane extended it. It asserts lib/tenancy/tenant-context.ts mentions neither RISING_ORIGIN_PACK nor its import, so the draft pack stays a draft mechanically rather than by comment. Mutation-confirmed: registering the pack fails the check.

        8.7 Phase 7 — make the application honour the pack

        Full specification textdetail for agents and careful review

        Named 2026-09-09 by what stage 1.5 found. Not exploratory. Four items, each already located:

        1. The board takes its pipeline from the pack rather than from PIPELINE_KEYS.
        2. Settle which of organization_memberships and person_organization_relationships is canonical for people on an organisation, then make interface and mapping agree. This is the lane's first task rather than a blocker, and Cody's decision line stands in FINDINGS.md either way.
        3. Register the pack, deliberately, updating the assertion that currently forbids it.
        4. Stop seeding Ventari-specific organisations into a client stack REMOVED FROM PHASE 7, 2026-09-10. The audit found this is not a patch: it is 107 fact-bearing seed statements across 278 migrations, applied migrations cannot be edited, and a forward cleanup cannot tell a Ventari database from a client one because all three packs declare literalTenantId: 'ventari' and pack selection lives in request-host logic a migration never sees. It is an install-architecture decision, and it is Alex's to make. Phase 7's worker 03 correctly stopped rather than forcing it.

        Corrected in 1.5. Version 1.4 said this was "put to Cody and Nico rather than built". That was wrong, and it is worth being exact about why, because the technical half of item 4 is sound and only the ownership half was not.

        What blocks a fix is the third clause above: a forward cleanup cannot tell a Ventari database from a client one, so a migration written to remove "Ventari's seeded organisations" would run identically against Ventari's own production database and could not know which one it was on. That is a delete-live-data risk, and it is why this is not a patch.

        None of that needs a founder's sign-off

        What it needs is a decision on how a fresh client stack is provisioned: either a different migration baseline for new installs, or a marker that lets a database state whether it is Ventari's or a client's. Alex holds that call under D20. The founders' interest here is the security handoff, specifically the cybersecurity hire being brought in, for whom decisions-2026-09-10 is the briefing document. That is handoff material, not a permission gate.

        Exit, amended 2026-09-10: a Rising-Origin user opens their own seven-stage board with their own people attached, and Ventari's own instance is unchanged.

        and sees nobody else's clients — that clause moved out with item 4. The client still sees Ventari's seeded organisations, and will until the install-architecture decision lands. Phase 7's preview measured 39 in the database and 37 in the sidebar.

        Why this document was wrong. §8.7 was written before the seed surface was audited, and it assumed the leak was a Phase 7 fix. It is not. Caught by the Phase 8 operator at orientation, which refused to dispatch a lane whose scope contradicted this SPEC — correctly, because a SPEC disagreement is a stop condition.

        Only then is there something to show a client. The message already sent promises a demo and names no date, which is the right position to be in.

        Where this programme continues. Phases 1 to 8 are the code half of the original goal. The install half, and the crm-builder skill it ends in, is scoped separately in systems/ventari/crm-factory/SPEC.md, drafted 2026-09-10. Phase 8 is the last phase in THIS document.


        8.8 Phase 8 — The pack owns the chrome

        Full specification textdetail for agents and careful review

        Named 2026-09-10. Version 1.4's summary line claimed to name Phase 8 and then did not define it. Caught by the Phase 8 operator at orientation. This is that definition.

        Phase 7 made the application ask the pack for its pipeline and its contact list. Both work. Everything else still says Ventari, on a client's screen, under a client's branding.

        The mechanism already exists and is unevenly adopted

        lib/tenancy/branding.ts resolves theme tokens, terminology, labels and navigation from the loaded pack, and rising-origin.ts declares all of them. Layout metadata and the sidebar already consume it, which is why a client's tab title and sidebar are correct while sign-in is not. This is adoption, not design.

        Four surfaces, each located:

        That fallback is the one that matters. A client whose pack fails to resolve is served Ventari's fifteen stages, one of which is named after a member of staff and describes what he personally does. It fails silently: a board that renders looks fine.

        Decided 2026-09-10, and workers implement rather than reopen:

        1. The standard client pipeline is Rising Origin's seven, promoted: New / Contacted / Qualified / Proposal / Negotiating / Won / Lost, named Sales. Derived from a real client build; no Ventari vocabulary.
        2. ventari_sales is agency-only.
        3. Unknown pack fails closed. A known pack with no declaration gets the standard pipeline. Misconfiguration loud; an uncustomised client still works.
        4. Seeding is first-install only. Never overwrite a client's stages.
        5. Stage naming: no person's name, no internal process step, no other product's name.

        Exit: a check, broken on purpose for each class and wired into core-stability, proves no Ventari fact reaches a client surface — the mirror of verify-install-fixture-cleanliness, which proves no client fact reaches an install artifact. And Ventari's own instance is unchanged.

        Deliberately out of scope: the seed leak, which §8.7 item 4 holds open as an install-architecture decision of Alex's, not a Phase 8 change. Phase 8 can make an instance say Rising Origin on every surface and still ship Ventari's seeded organisations inside the client's database. Those are different problems.

        No rename lane. Nico's Helix brief of 2026-09-09 is explicit that technical paths keep growth-os and nothing is bulk-renamed. The product name changes; identifiers stay.

        Lane: sessions/estate-map-pack-owns-chrome-2026-09-10/.

        Ventari Estate Map · Operating standard

        Requirement priorities

        Using the priority classes already in use in Ventari specifications.

        9.1 P0 — Non-negotiable

        • Phase 4, the check audit. Independent of the map. 824 checks exist, 34 run, 159 cannot be invoked at all, and nobody has established which matter. (Confirmed, re-measured 2026-09-09 at app df62cfa. Deletion left this phase's scope the same day; see §8.4.)
        • Phase 5, tenant isolation proof. Ventari's own custody plan records isolation as unproven and shared-RLS cutover as blocked. (Documented.) If it is wrong once it is wrong in every client build.
        • Principle 6. No phase ships prose alone.

        9.2 P1 — Required for the map to be worth building

        • Phase 1, census.
        • Phase 2, regions.
        • Phase 3, blast radius.

        All three or none. A census without blast radius is a list, and lists are what the estate already has.

        9.3 P2 — Expansion

        • Phase 6, the reusable client core. Real value, no risk in deferring.
        • A machine-readable projection of the map for the Executive. Moved to Phase 2 (VE-179), corrected 2026-09-07. Version 0.1 said not to build this until a consumer existed. The consumer already exists: the Executive's prompt assembly reads nothing from the canon today, and North Star §4 requires operational agents to act with the estate in view. Scope is a small server-owned read of the region index for the regions a task names, never the whole map in every prompt. Entry point is the prompt assembly documented in the app's docs/executive-context.md. Blast radius is not part of this read until Phase 3 builds it (12.3, superseded 2026-09-08).
        • Extending the map to repositories owned by a client's own GitHub organisation. Out of scope until a second installation exists (H7), per 12.6 and 12.9. This never excluded client project repositories: those sit under joyblisscoder and are already in scope and in the census.

        Ventari Estate Map · Operating standard

        Acceptance criteria

        10.1 The map as an artifact

        • Every card carries type, universe, status, verified_at, verified_revision.
        • No card restates a count that a generated file already holds.
        • No card names a credential value.
        • Every generated file is rebuilt by a named command and is never hand-edited.
        • A validator fails the build when a card cites a path that does not exist.

        10.2 The map as a tool

        • A cold reader names every live region and its boundary from one page.
        • For any named region, the map returns what a change to it touches.
        • Every repository, hosted project and server carries a universe label.
        • Two hops answer any "what is X". Three means a card is missing.

        10.3 Checks and gates

        • Every verification script carries a recorded verdict.
        • Every surviving check runs without a human choosing to run it.
        • Each protected action has a test that attempts it and is refused.
        • Isolation is proven with two synthetic tenants, not asserted.

        10.4 Maintenance

        • Regenerating every generated file is one command.
        • The hand-written layer fits on one page.
        • A stale card is visibly stale rather than quietly wrong.

        Ventari Estate Map · Operating standard

        Anti-patterns

        11.1 The failure this project must not repeat

        (Confirmed, and stated by Ventari about itself.) The core-stability workflow carries this comment:

        Until 2026-09-06 nothing ran these automatically: 551 verify scripts existed, 0 ran on a push, and the Client Health snapshot contract drifted for four days unnoticed.

        803 well-named scripts, written to a consistent method, and nothing read them. Structure did not save them. A map is pure artifact and is more vulnerable to this than a test suite, not less.

        This is why principle 6 exists, and it is the one rule that should be treated as non-negotiable.

        11.2 Other red flags


        Ventari Estate Map · Operating standard

        Estate decisions

        Full specification textdetail for agents and careful review
        Closed 2026-09-07

        All six were answered as decisions of record (D20), ratified by Nico, in Reports/Ventari Software Evolution/2026-09-07-estate-map-go-brief-Alex.md. The rows below record the ratified answer, not this document's proposal, and several differ from what version 0.1 assumed. If one proves wrong in practice it is raised in the claim issue with evidence and gets a dated supersession, never a silent edit.

        12.7 Raised back, not settled

        • The Brain has no gate at all. 12.4 leaves VentariBrain unprotected for a real reason: agents and the sync bridge push records directly. The consequence is that the canonical company memory accepts unreviewed automated writes. That belongs in FINDINGS.md as an accepted risk with a name against it, not as a settled non-issue.
        • Counts move. Core stability now runs 17 checks, not 16, since the vault path verifier landed. Any figure quoted without a commit is already stale, which is the argument for the citation rule in the first place.

        12.8 Superseded in part: 12.3, dated 2026-09-08

        What 12.3 ratified: the Executive read returns the region index plus blast radius for the regions a task names, in Phase 2.

        Why that cannot stand: blast radius does not exist until Phase 3. §8.3 builds it and §8.1 of the program register assigns Phase 3 to VE-180, which depends on VE-179 completing. As written, Phase 2 consumes an artifact that Phase 3 produces.

        The correction: Phase 2's Executive read returns the region index only. Blast radius joins the read in Phase 3, as part of VE-180. Nothing is dropped — §9.2 already requires all three phases, "all three or none."

        Whose error this is

        Not the review's. Version 0.1 placed the Executive read in P2 and said not to build it. Version 0.2 moved it into Phase 2 and, in doing so, carried a "plus blast radius" clause that contradicted §8.2 and §8.3 two pages earlier. The contradiction was drafted here and then ratified along with everything around it. It is recorded as a supersession rather than edited away because §12's own rule requires that of any D20 row, and because the ratified text is what Nico read.

        Standing: applied. This corrects a self-contradiction in the document, not a judgment call, so it does not wait on a decision. Raised in the claim issue for visibility.

        12.9 Clarified: 12.6, dated 2026-09-08

        Full specification textdetail for agents and careful review

        Two phrases in 12.6 were doing more work than their wording admitted. Neither meaning changes here; both are written down so they cannot be misread again.

        H7, quoted. From Nico's go brief, section 2, the Phase 6 row. A read-only copy is kept beside this spec at go-brief-2026-09-07.md, because the brief was not reachable from any local source when these citations were checked:

        H7 second installation from configuration

        From configuration is the operative phrase. H7 is satisfied by an installation produced by configuring the existing app, not by copying it. That is the tenant-pack mechanism in lib/tenant-packs/, and the same brief names its candidate in the same line: D14 (Helix is installation two).

        H7 is not satisfied today

        Both shipped packs, ventari-agency.ts and helix.ts, declare mode: 'single_agency_literal' with literalTenantId: 'ventari', isolationProof: 'unproven', saasReady: false and saasClaim: 'not_claimed'. Helix is pinned to Ventari's own tenant id and the code declines to claim multi-tenancy. So the gate 12.6 describes is closed, and Phase 5 is what opens it: isolation proven is the precondition for a pack becoming an installation.

        joyblisscoder/growth-os does not satisfy H7

        , because it is a fork rather than a configuration: a separate repository, deployed separately, with its seams in brand.config.ts, public/brand/* and .env. It is raised in the claim issue under a different heading, because the brief's Phase 6 row also says no second reusable-core track.

        "Client-owned repositories" means repositories in a client's own GitHub organisation

        , code Ventari does not own and cannot enumerate. There are currently none. It has never meant repositories holding client work: those sit under joyblisscoder, and all 103 census entries do. cvc-admin, cvcwellness-site, risingorigin, helix-site, helix-library and the five holistic* repositories are in scope and already carry universe labels. An agent asked about a client project can see them today.

        Ventari Estate Map · Operating standard

        Correction

        Ventari Estate Map · Operating standard

        Estate evidence

        Read on 2026-09-07 from committed code, generated documentation and repository settings. Nothing was modified.

        13.1 Confirmed by direct inspection

        • VentariFullApp at origin/main, commit 1771e2f.
        • 95 repositories enumerated via the GitHub API.
        • Branch protection queried on five core repositories. All returned "Branch not protected".
        • .github/workflows/core-stability.yml and verify-advisory.yml.
        • scripts/core-stability-manifest.json: typecheck plus 15 checks.
        • 803 verify-* scripts and 718 npm script entries.
        • vercel.json: 51 scheduled jobs.
        • docs/calendar-control-plane/, docs/client-health/, docs/work-ledger/.
        • PRODUCT.md, DESIGN.md, CONTEXT.md, lib/client-brain/librarian.ts.
        • VentariBrain at origin/main: 5,949 files.

        13.2 Recorded as Ventari's own documented claims

        • docs/architecture/multi-tenant-custody-plan.md: 368 tables with a tenant column, 394 functions hardcoding the literal tenant, isolation unproven, shared-RLS cutover blocked, live migrations drifted from repository files.
        • ADR-005: reusable core accepted 2026-08-19, two follow-ups blocking, install fixture "Not run".
        • ADR-006: single-agency subtree isolation, production isolation proof pending.
        • docs/paid-client-fulfillment/: route convergence map and surface inventory.
        • docs/executive-context.md: prompt assembly inventory, compiled 2026-09-06.

        13.3 Not reviewed

        • The live database. Repository migrations are not production evidence.
        • Live hosting configuration, environment values, and which scheduled jobs are enabled.
        • The audit engine server and the two executive hosts, as running systems.
        • The contents of the 803 verification scripts. They were counted and grouped by name. Reading them is phase 4, not this review.
        • Whether the 16 running checks are currently reliable.

        Prepared by Suzuki Systems for Ventari, 7 September 2026. Proposed, not approved. Edit this file; the HTML view is generated from it and any change made there is discarded on the next build.


        Ventari Estate Map · Revision record

        Shipped work

        Full specification textdetail for agents and careful review

        Two findings from this review are closed. Recorded here so the phases below are not planned against problems that no longer exist.

        Also fixed in passing: build-indexes.mjs built entity paths with path.join, so running it on Windows rewrote every row of all three generated indexes. Now path.posix.join; byte-identical output on macOS and Linux.

        Consequences for the counts in this document. 17 checks now run automatically, not 16, so 783 remain unclassified rather than 784. Any figure quoted without a commit is already stale, which is the argument for the citation rule.

        Two pre-existing issues surfaced and left alone, both belonging in docs/estate/FINDINGS.md rather than in this work:

        • Leads/Christine Estrema Sun has no Context.md, only a proposal draft, so the full Brain filing check fails on main. CI misses it because the workflow runs the staged-file check rather than a full scan.
        • Booking production contract gate has been failing on VentariFullApp main roughly eight times a day, predating this work. A workflow that fails constantly on main teaches people that a red mark means nothing, which is the condition that let the merge gap survive.

        Ventari Estate Map · Revision record

        Revision v0.2

        Full specification textdetail for agents and careful review

        Applied 2026-09-07 from Nico's review. Three were errors in 0.1; the rest is context that was not reachable from the code.

        Process note

        The two pull requests that came out of this review were opened before their claims were filed, contrary to the protocol in VentariFullApp issue #277. Claims were filed retroactively (VentariFullApp #499, VentariBrain #79) under the protocol's own mid-session rule, and nothing was merged in the interim. Recorded here rather than quietly corrected.


        Ventari Estate Map · Revision record

        Revision v0.4–1.4

        Applied 2026-09-08. One correction, found while planning Phase 2 against this document rather than from memory.

        How this was missed

        Version 0.2 moved the Executive read from P2 into Phase 2 for a good reason — the consumer already existed — and carried its original wording across without re-reading §8.2 and §8.3. The clause was internally contradictory before it was ratified. A review that is checking whether a plan is right will not always catch a plan that disagrees with itself, which is an argument for reading a spec end to end before building from it, not only when writing it.

        Also noted, not changed. §9.3 places the Executive read in P2, not P1. By this document's own priorities Phase 2 is worth shipping on the region cards alone, and the read is expansion.

        0.5, applied 2026-09-08

        What the H7 wording settles

        "From configuration" means H7 is about configuring the existing app, not forking it, so the tenant packs are the mechanism and growth-os is not. It also means H7 is not satisfied today: both packs are pinned to literalTenantId: 'ventari' with isolation unproven. Phase 5 is the gate that opens it. This closes a question this document had been carrying as open.

        Where H7 was found, and why that took a second attempt

        In Nico's go brief, section 2, Phase 6 row. It was searched for in the Brain repository, the mirror, the operating desk and this Brain, and it is in none of them: the local Brain mirror predates the brief's publication. The lesson is that the approval document is itself a source and belongs beside the spec, not only in the thread it arrived in.

        0.6, applied 2026-09-08

        Both changes came out of building Phase 2 against this document, which is the only reliable way to find what a spec left ambiguous.

        A note on where the region names came from

        Regions derive from the registry's own System ownership table and extend it only where the census found something live with no home there. That produced twelve regions covering all eighteen live entries exactly once. One name was rejected: growth-os is recorded as a member repository, and the region is Client core, because two dated founder decisions forbid naming internal architecture "Growth OS". The Phase 2 card verifier enforces that by name rather than trusting anyone to remember it.

        0.7, applied 2026-09-09

        Full specification textdetail for agents and careful review

        Phases 1, 2 and 3 have shipped. This version changes the phase that is next, before it is built, rather than after.

        Why the "stale" verdict got harder, not easier

        Every one of the 824 scripts was last touched in August or September 2026. There is no dusty corner to sweep, so age proves nothing and stale has to mean tests something that no longer exists or duplicates another check. Both cost real work. Neither is urgent. That is most of the argument for deferring deletion on its own.

        A phase-shape lesson worth keeping. Phase 2 was built against a document that contradicted itself, and the contradiction was found during the build. This amendment was written before the Phase 4 lane, deliberately, for that reason.

        0.8, applied 2026-09-09

        Full specification textdetail for agents and careful review

        A3 fails. Phase 5 ran a two-tenant fixture on a local database and staff of installation B read and wrote installation A's vault and tasks, with the writes persisting. §8.5 carries the evidence, the cause and the measurement.

        The finding underneath the finding. The correct predicate already exists: foundation_can_access_tenant is is_staff() AND tenant matches, built in migrations 0065/0068/0069. It is adopted on 16 of 226 staff policies and the rollout stopped. The check that proves it works passes today and is orphaned with no npm script.

        So this was never a missing design. It is a design that was built, tested, and left 92% unapplied with its proof unrunnable — which is precisely the class of thing the estate map exists to surface, and which no single repository shows.

        A note on what this is not. Not a live breach. One tenant, and staff-global access to Ventari's own data is correct for single_agency_literal. Client-seat isolation held. The custody plan's isolationProof: unproven and sharedRlsCutover: blocked were accurate all along; Phase 5 is the measurement behind them.

        0.9, applied 2026-09-09

        Full specification textdetail for agents and careful review

        Phase 6 is not blocked, and 0.8 said it was. The correction came from reading three pages further into a document Phase 5 had already opened.

        What A3's failure still means

        The measurement stands and the leak is real: staff of B read and wrote A's rows, reproduced twice, independently. It matters if Ventari ever adopts shared-database multi-tenancy. The custody plan rejected that as the cutover strategy, so it is not the critical path to the business model. Calling it that was wrong.

        The two walls, which this phase conflated. Between peer agencies: a separate database each, nothing to leak. Between clients inside Ventari: the organization_id subtree, which held in the test.

        The finding that most deserves a team conversation

        Both client CRMs that have shipped are clone-style, and neither used the chosen tier. The likely reason is timing, not disagreement — growth-os was extracted on 2026-09-05, after the Rising Origin build, so there was no reusable core to reuse. A decision that is made, documented, and unexecuted twice is worth more attention than a decision that is open.

        1.0, applied 2026-09-09

        Full specification textdetail for agents and careful review

        Phase 6 gets a name. It always said derive contents empirically from the one real client build. Rising Origin is that build, and §8.6 now says so.

        Why the split. Rising Origin is a live system a paying client depends on. Every phase before this was additive or measurement; this is the first that would touch something in use. Prove it locally, then propose — which is the rule that has held for five phases.

        The offer, for the record, because it is a founder's call and not an engineering one

        The client keeps their database, Ventari owns the codebase, and features are switched on rather than rebuilt. That is decision D19 stated as a product. It is a conversation to have with them, and it is much stronger after stage 1, because it shows a working instance rather than describing one.

        Version 1.0 rather than 0.10. Six phases, five shipped, and the sixth now names its own test case. The document has stopped being a proposal.

        1.1, applied 2026-09-09

        Full specification textdetail for agents and careful review

        Phase 6 stage 1 ran, and the answer it came back with is smaller than the question. §8.6 now records what was measured rather than what was planned.

        What this phase kept getting right by accident, and should keep doing on purpose

        Every load-bearing claim in stage 1 was checked against source a second time by someone who had not produced it. That is how the 7-of-14 migration list, the leads duplication and the 482 framing were all caught, and none of the three was caught by the person who wrote them.

        1.2, applied 2026-09-09

        Full specification textdetail for agents and careful review

        A stage was missing between knowing the cost and proposing the move.

        Why the correction is in the spec rather than quietly fixed in the report

        Six phases in, the failure this map keeps finding, in others' work and in mine, is a confident reading of one source contradicted by the next source along. The gap map's conclusion was right and its reasoning was incomplete, which is the harder version of that failure to catch, because nothing downstream looks wrong.

        1.3, applied 2026-09-09

        Full specification textdetail for agents and careful review

        The dry migration ran, and it separated two answers this document had been giving as one.

        What this phase is now evidence for

        Every phase of this program has found the same thing: a claim that held against one source and not against the next. Stage 1.5's version is the most useful yet, because the claim was this document's own, it was correct as far as it had been tested, and the untested half was the half a client would have seen first.

        Document 2

        Client CRM Factory

        12 sections · Phases 9–14 · Factory v0.11

        Part II · Phases 9–14

        Client CRM Factory

        The operating standard for building, launching, and improving every custom client CRM instance

        This is the installation, module, evidence and release standard for every CRM Factory instance.

        Client CRM Factory · Build standard

        Overview

        Full specification textdetail for agents and careful review
        • Live client: distinguish Rising Origin's last stable GitHub Release (client/rising-origin/v37) from its latest manually CLI-promoted Vercel production deployment, built from 788cd1bd without a new stable Release. See §4b for both records and §13e for the remaining GHL-exit path.
        • Connections: the shared analytics bridge, optional GA4 adapter and Meta measurement support are included. Provider setup and first-party tracking still require their own connection and rollout proof. The X OAuth repair is in v37; one fresh authorized reconnect must still prove persistence.
        • Current program: the former #969, #974 and #975 ownership gates are closed. Repeat preflight against current main before any new design, mobile, speed or functionality pass. Start at §4b and the dated checkpoint.
        • Phase 14 focus: Phase 14 is not started and must not start until the gates in §4b Phase 14 gates pass. The ordered install sequence (runbooks/install-sequence.md) was reconciled with the CLI release standard in #1073 and stays status: candidate and inactive until that proof passes.
        • Authority: edit docs/crm-factory/SPEC.md with the code it governs. The checked-in HTML is generated and verified from this file.

        The goal, unchanged since the estate map opened: be able to install all the roots for any CRM build we make for a client.

        Phases 1 to 8 answered the half of that question that was about code; they live in the estate-map spec (docs/crm-factory/ESTATE-MAP-SPEC.md, approved by Nico 2026-09-07 as D20) and are not restated here. This document is the second of two, not a copy of the first, and its numbering continues from it. It covers the half that is about installing, and it ends where the whole programme has been pointing: a skill in the agents tab that any agent doing client work can find and run to build a client CRM properly.

        The skill is the last phase, not the first. Writing a skill for a sequence nobody has executed is the exact failure this programme spent nine days correcting. Do not start Phase 14.


        Client CRM Factory · Build standard

        Architecture standard

        Full specification textdetail for agents and careful review

        Merged to main on 2026-09-10 as PRs #549, #550 and #543.

        The architecture is done. Nothing below needs a new idea. What remains is plumbing, and plumbing is where "we can stand up a CRM in an afternoon" either becomes true or stays a story.

        Beeem is a fast exception, not a second proof of this architecture

        Its live CRM was assembled quickly in joyblisscoder/beeem-crm with its own app code, Shopify webhooks, dedicated Supabase project and Vercel Git deployment. That speed is useful evidence for the factory. The separate source tree is not the one-codebase client-pack standard above, is not in deploy/clients.json, and did not ship through client/<packId>/v<n>. Until those reusable pieces are brought into the shared core, Beeem must not be counted as the independent second factory installation required to freeze the runbook or start Phase 14.

        Corrected 2026-09-29: the client/<packId>/v<n> tag path is now parked (§13a.1); Beeem is excluded because its source tree is separate and it is not in deploy/clients.json.

        Client CRM Factory · Build standard

        Installation baseline

        Full specification textdetail for agents and careful review

        This table is the original measured baseline. It is retained as evidence, not as today's installation checklist.

        Row 2 is a pull request. REGISTERED_PACKS in lib/tenancy/tenant-context.ts is a hardcoded TypeScript array, and loadTrustedHostBindings silently drops a binding that points at a pack not in it. Until that PR ships, the client's domain resolves to nothing and their env binding is ignored without an error.

        Row 7 was worse than "polluted", and this document said the wrong thing. Running supabase start on a clean stack on 2026-09-10, which nobody had done, found two defects that made a fresh database impossible to build:

        • 20260828173000_work_graph_delivery_state.sql opened with a bare LOCK TABLE, which Postgres rejects outside a transaction. It died at migration 264 of 279.
        • Two migrations shared version 20260907050000, and schema_migrations.version is a primary key.

        Both fixed. All 279 now apply, and that is the first time this database has been built from scratch. The duplicate version carries a production question: hosted can only have recorded that version once, so one of the two migrations may never have reached it. Checking whether public.telegram_reminders exists in hosted settles it.

        Then the seeding, which is the part this document already knew about. 0006_collaboration_oracle_analytics.sql inserts Ventari's own client organisations and 0002_events_notifications_pricebook.sql inserts Ventari's price book, into every database that runs them.

        Measured on the first clean build, 2026-09-10. A brand-new client database contains 21 organisations, all Ventari's; 16 price book entries, Ventari's pricing; 32 stage definitions across 3 pipelines; and 0 opportunities. Those are Phase 11's acceptance numbers and they must all reach zero.

        Row 8 has an operating answer: Supabase access is part of the installation baseline

        The client invites the installation operator to the client-owned Supabase project. That access lets the operator plan, apply, and verify each pack-aware migration against the correct instance. Without the invitation, the Factory cannot certify that the instance is current. Fan-out remains an explicit operator action so production authority stays visible rather than becoming a silent background mutation.

        Proposed additional database-hosting path (not the default)

        An Executive may prepare a Ventari-hosted project request containing the client, pack, region, planned action and monthly-cost note without creating anything. A named admin must approve the exact Client Program plan with an expiring grant before the server can create the project in Ventari's configured Supabase organization. The Executive prepare action/card is future work: no such action or card is implemented, and this slice does not enforce that a prepared request exists. Client-shaped migrations, instance identity, foreign-tenant cleanliness and the install receipt remain a separate, not-yet-implemented setup stage. Until those pieces are built and proved, the client-owned path remains the operating default.

        Current state, verified 2026-09-24: much of the repeat work is now guarded and automated, while account ownership and production authority stay human.

        What Beeem proves

        a narrowly scoped CRM can go from a reusable pipeline pattern to a live, protected, Shopify-connected app very quickly. It also shows the remaining standardization gap: its useful Shopify ingestion, capture and newsletter behavior live in a separate application, so shared-core upgrades do not automatically reach it. The factory should learn from that speed without mistaking a parallel codebase for a completed canonical install.

        Current program state is §4b; the dated table above remains the before picture.

        Client CRM Factory · Build standard

        Implementation phases

        Ordered by dependency, not by appetite. Phase 9 comes first because it is the only one that produces measurements rather than assumptions.

        Accuracy note, 2026-09-24

        The phase bodies preserve the requirement and the evidence available when each phase was written. They are not a claim that the work later closed in numeric order. Current state overrides old future tense: Phase 9 is complete; Phase 10 is partial because registration remains a reviewed code change; Phase 11's maintained client snapshot and drift gate are not evidenced complete; Phase 12's pack-aware migration fan-out is operational; Phase 13 is active across release, pruning, connections, intake, booking, assistant and reporting; Phase 14 has not started. §4b is the current ledger.

        Phase 9 - Migrate Nick, and write down every step as it is taken

        Not a build phase. An observation phase. Rising Origin is migrating anyway and he sounded happy about it. Doing it by hand, with every step recorded, produces the only honest input the later phases have.

        • Execute rows 1 to 7 manually for Rising Origin.
        • Record every command, every value, and every place a decision was made, at the time rather than afterwards.
        • Where a step needs a judgement call, write down what was chosen and why.
        • Remove Ventari's seeded organisations and price book by hand, and count what was removed. That count becomes Phase 11's acceptance target.

        Exit: Rising Origin is live on the reusable core, and a written sequence exists that a second person could follow without asking a question.

        Explicitly not in scope: automating anything. The temptation to automate while executing is how you get a script that encodes one client's accidents.

        Phase 9b - Cutover for a client who already has a system

        Full specification textdetail for agents and careful review

        Added 0.3, 2026-09-10. Phase 9 assumed a client arriving with nothing. Rising Origin arrives with a live Supabase, fourteen migrations of their own, and a team already signed in. That is the harder case and it is the one worth rehearsing, because a client with no existing system is trivially easy.

        The decision that shapes everything: reuse the client's Supabase project, or create a new one.

        auth.users lives in the auth schema; the core's 279 migrations only ever touch public. So reusing the client's project keeps every existing login working for free, and identity migration stops existing. Alex made this point directly: he is already in Nick's system. A fresh project would mean exporting and importing auth.users including password hashes, which nobody here has rehearsed.

        What reuse costs: in-place rollback. The core wants names that are already taken in public, so applying it is a hard cutover with no parallel run. A verified snapshot is the only way back.

        Recommendation: reuse, with a rehearsal on a restored snapshot first. For a single client with one team, a snapshot is adequate rollback, and identity is the messier of the two problems.

        The procedure

        Run npm run inventory:client-collisions -- --url "<client db>" first. It is read-only, parses the core's objects from the migrations, introspects the client's public schema, and prints exactly what clashes plus the rename statements. Point it at a restored snapshot before the live database.

        1. Snapshot. A gate, not a step. It is the only rollback.
        2. Rename the colliding objects to a <client>_legacy_ prefix. Postgres resolves foreign keys, indexes, policies and column types by OID rather than by name, so every relationship inside the client's schema survives a rename.
        3. Apply the core's 279 migrations to public, now that the names are free.
        4. Transform from the _legacy objects and the client's untouched tables into the core's shape, using the mapping stage 1.5 already proved.
        5. Verify counts against stage 1.5, then remove the seeded Ventari rows: 21 organisations and 16 price book entries as measured on 2026-09-10.
        6. Repoint the client's deployment at the shared codebase with their host binding.
        7. Walk the instance with the client before dropping anything.
        8. Drop the legacy objects, last, and only after step 7.

        The client's application is down from step 3 to step 6. Minutes at Rising Origin's volume, and it is a genuine outage rather than a DNS wait, so it is announced and scheduled.

        The client's old schema does not need to keep working. The transform reads as service role, which bypasses RLS, so their policies, functions and views can be renamed or broken without consequence. What is needed is their data legible for one pass, not their application functional.

        Worked example, Rising Origin, measured 2026-09-10

        32 objects in their public schema. Six collide: tables organizations, people, notes; types organization_status, person_status; function current_person_id. The other 26 can sit untouched while the core installs.

        The inventory script reproduces that set exactly, which is how it was verified: it was first worked out by hand, then the script had to agree.

        The undocumented step this exposed

        Creating the first admin user has no script and is not written down anywhere. It takes five steps against a fresh database, two of which are not guessable:

        1. POST /auth/v1/admin/users with the service-role key
        2. INSERT INTO people
        3. INSERT INTO profiles with id = the auth user id, linked to that person
        4. INSERT INTO role_assignments with role admin
        5. PUT app_metadata with ventariRole, ventariRoles, ventariOnboarded

        Miss step 5 and the user sits on /pending forever with no error shown. This was found by hitting it during the 2026-09-10 preview. It belongs in the runbook and then in the skill, and it does not apply when reusing a client's project, which is another argument for reuse.

        What round two found on real data, 2026-09-11

        The procedure above ran twice: once on the synthetic fixture (a Grok worker), once on Rising Origin's real export (Alex, with Claude driving). Both are in docs/crm-factory/runbooks/cutover.md. Every step held on real data, and the real run added five rules the synthetic one could not:

        1. The export is the truth. A client's own migrations seed reference rows (Rising Origin: a role matrix and one initial identity). The export carries those rows as production has them. The loader truncates first. The real cutover has this same collision.
        2. Free-text in a closed-dictionary column. Rising Origin's team typed job titles into the membership role field. The mapping refused ten of thirteen links, correctly. Decision: unrecognised roles map to other and the title is kept on the person as metadata. Expect every client to have one of these. The refusal count is the signal; a mapping that silently coerces would have hidden it.
        3. The bootstrap admin checks for an existing person first. Alex was already in Nick's data as a collaborator; the bootstrap created him again.
        4. The sidebar CRM surface must point at the org list, /crm/clients, not the client-360 vault at /clients, which is empty on a fresh instance.
        5. lib/team.ts puts Ventari's six people on every client's Team page. A code constant, not a pack surface, not seed data. Third instance of that pattern; it needs the same fix as the executive name and the icons.

        The export path removes the password dependency for the rehearsal. A collaborator holds the service-role key and can read every table through the REST API. scripts/export-client-data.mjs does that, read-only. The password is still needed for the live cutover, because that applies migrations.

        Screenshots of his real pipeline exist under snapshots/, gitignored, for the message to Nick. His deals, his stages, his company names, the pack's chrome, and the workspace strip showing only what his pack declares.

        Phase 9c - Brand fidelity: the token pass

        Full specification textdetail for agents and careful review

        Added 0.5, 2026-09-11, from Alex's screenshot of Rising Origin's current build next to the new CRM. Runs alongside Phases 10 to 13; it neither blocks provisioning nor waits for it. It does block the skill's design step in Phase 14.

        The finding

        After the brand and font lanes, Rising Origin's instance carries his name, logo, favicon, share card, typefaces and one accent colour. It still reads as Ventari with his logo on it. His current build uses orange structurally: a border on the active nav item, outlines on toggles and status pills, a solid fill on the primary button; headings in condensed capitals, labels in a monospace. None of that reached the new instance, because the pack expresses brand as data and the application expresses design as code, and data only lands where code reads it.

        Measured 2026-09-11: components/ and app/ contain 9,788 hardcoded Tailwind colour classes across 380 files (bg-zinc-800, border-white/10, text-zinc-400 and kin). The pack's six colour tokens are read by a handful of them. That is why setting primary to his orange changed a button and a number and nothing else.

        What his own CSS already declares

        , the extraction source: --color-background, --color-foreground, --color-muted, --color-accent, --color-border, --color-surface, --color-surface-raised, --shadow-focus-ring, --font-body, --font-display, --font-metadata, --radius-sm, --radius-md. Thirteen tokens, nearly one to one with what the contract below needs. That is the argument that extraction can be automated.

        Four steps, in order. The order is not negotiable.

        1. The token contract. Decided by Alex 2026-09-11: the table below, as proposed. Names follow the code that already exists (primary, not accent; the six signal* keys stay as the status group), because renaming a live key is churn with no client on the other side of it.

        The agency's values for the new keys are measured, not chosen

        Counted 2026-09-11 in components/ and app/: border-white/10 812 times, bg-white/5 171, bg-zinc-800 140, bg-zinc-900 88, text-zinc-500 1,788, text-zinc-400 923, text-zinc-600 909, #ffaa00 156 plus amber-400 171, rounded-lg 1,138, rounded-xl 543, uppercase 579, tracking-wide 234 and tracking-widest 154. A token's agency value is the colour the dominant class already resolves to, alpha included (border is rgb(255 255 255 / 0.10), not a hex guess), so that converting the class to the token changes no pixel.

        The typographic roles are the part the current contract cannot express and the part that makes his build look like his build. A family alone does not give you condensed capital headings and monospace labels.

        2. Surface conversion, one lane per surface group, parallelisable

        Shell and sidebar; funnel; CRM and contacts; team; agents and chat; auth and onboarding. Each lane converts the hardcoded classes in its group to token classes. Ventari is unchanged by construction, and the rule that makes it so is mechanical: a class is replaced only by a token whose agency value is exactly the colour that class resolves to. A class that matches no token is residue: counted in the report, left alone, never approximated. The proof is not a byte diff of the HTML, because the class names change by design. It is a pixel diff: a fixed set of routes on the synthetic fixture, screenshotted at two widths before and after, zero differing pixels, plus computed-style equality on a sampled set of elements. The harness that takes those screenshots is built once, in the contract lane, and every conversion lane runs it. A lane whose agency pixel diff is not zero is rejected. Grok can run these; the seal is the diff.

        3. The extraction script

        Read-only. Given a client's CSS, a file or a URL to their current build, parse its custom properties and propose a pack token block, with a confidence per token and a list of what it could not infer. His globals.css is the first test case: the proposed block should reproduce his thirteen tokens without a person typing them. This is the automatable part, and the part described to the team as agents getting the brand guidelines integrated.

        4. The side-by-side gate. A step in the skill, not a lane

        The extracted tokens rendered on the new CRM next to a screenshot of the client's current build. A person adjusts what extraction cannot decide: where accent is a border and where it is a fill, whether headings are capitals, how status pills look. This is the human design pass and it stays human on purpose. Alex's screenshot of Rising Origin's current build is the acceptance image for this client: after the gate, the new CRM on his tokens should read as the same product.

        The gate happened 2026-09-13

        , on the live instance, from Alex's screenshots of his old build next to rising-crm.vercel.app. Carried already: logo, Barlow body, mono kickers, input wells, outlined pills, the Invite button, the active nav on Team. Seven decisions, in order of how much each makes it read as Ventari: (1) lime literals on the funnel's stat tiles and UNASSIGNED pills where his orange belongs; (2) no solid primary button on the funnel or CRM, one per card as his build has; (3) flat square cards where his are raised and rounded; (4) raw status slugs in the CRM list instead of outlined pills; (5) the selected row's accent left edge; (6) page titles not in the display role; (7) the active nav item rendered two ways. One lane, historical on the operating desk (not required for pickup), agency pixel-identical; queued behind the Connections lanes for machine room. Two scope notes it surfaced, not design: the org workspace shows Ventari's full tab row on his pack, and Clients is now off (#598).

        Layout is out of scope. His two-panel CRM is structure, and a pack does not swap components. The pass makes the same structure wear his clothes completely. A client whose layout must differ is a bespoke build and is priced as one.

        Exit: the funnel, CRM and sign-in pages of Rising Origin's instance, on tokens extracted from his CSS and adjusted once through the gate, pass a side-by-side review by Alex against his current build. And the agency's rendered output is byte-identical to before the pass began, proved per lane.

        Phase 9d - A surface is on when it is his, not when it is visible

        Full specification textdetail for agents and careful review
        Added 2026-09-13, scoped the same night Rising Origin went live

        The surfaces lane turned SEO & GEO, Social Media, Clients and Inbox on for his pack and found each one still assuming the agency: Ventari's X handle as the account to connect, no Search Console property, the agency's client-health pipeline key, no mailbox, and tenant_id literals of 'ventari' in growth queries. Alex's words on the first sign-in: Socials says log in to ventaridigital. Declaring a surface (Phase 10) is not connecting it.

        The rule

        a surface's identifiers come from the pack, its secrets from the instance's environment, and nothing falls back to Ventari. The pack contract gains an integrations block (Search Console property, social account handles, mailbox address, client-health pipeline key, tenant literal); null means not connected, and the page says so in the client's name with the step a person takes. The agency's values are today's literals, so nothing moves on Ventari, proved per lane by the pixel harness.

        Two lanes (historical on the operating desk; not required for pickup): A for growth and social, B for inbox and clients. Both also answer Alex's first-sign-in notes: where attribution went (his old CRM had it on every lead; the mapping imported the touches), whether Clients is redundant on an instance with no client-health pipeline, and why Funnel lands on the control room.

        Connecting the real accounts is the client's own step, inside their instance. Revised 2026-09-13

        after Alex: each handoff of a password is an exposure; give him his own login within the app. Platform secrets (the Google OAuth client, the X developer app, the Resend key, the token encryption key) are Ventari's, registered once, injected per instance by the deploy action, never handed to anyone. Client accounts are connected by the client's admin on /settings/connections: Mailbox (verified over IMAP and SMTP, then the password stored encrypted in their database with the platform key, the same path as Google refresh tokens), Sending (from identity, with the DNS records to add shown for them to copy and a Verify button; decided 2026-09-14: a client enters their own Resend API key on the card, stored encrypted like the mailbox password, and owns their sending reputation; a client's own key is used with no platform fallback on client packs), Google and X (the existing OAuth flows). One resolver, stored connection first, environment second; the agency has env and no row and resolves byte-identical. The pack's integrations values are guardrails where declared and otherwise filled by what got connected. No env var per client; no lane holds a credential; the next client gets the same page. Landed as #602 (storage, API, migration 20260913120000) and #603 (the page). The three literal Ventari from-addresses in lib/email.ts went through the same resolver, closing the send-identity gap.

        Landed 2026-09-13

        , both lanes merged (#592 inbox and clients with the contract, #593 growth and social), fourteen zeros on the agency each. The lanes also answered the three first-sign-in notes: attribution's ledger and UI were built in the old Rising Origin checkout and never entered this core (the cutover projected his events onto touches; automatic attribution for him is a small lane over the form source, GHL's payload and the touches, not a ledger); Clients is an honest empty 360 on an instance with no client-health pipeline, recommended off for him until he has one; and Funnel opened Ventari's lead desk because /pipeline was a static redirect, fixed by making it a page that lands by pack (#594). The hand-steps to connect his real Search Console, X and mailbox are in the two reports; they are the remaining exit condition.

        Email, clarified 2026-09-13 (Alex: "we're doing email through Resend")

        Two different things in the app. Sending is Resend already: RESEND_API_KEY and RESEND_FROM_EMAIL on the instance, the client's domain verified in Resend; every notice, reminder and chase the CRM sends goes that way, no mailbox password anywhere. The Inbox tab is separate: it reads a mailbox over IMAP and replies over SMTP with that mailbox's password; the Resend inbound webhook does not feed it (it only forwards received mail to a founder address). So Resend-only means no Inbox tab, which is fine for a CRM that sends and tracks. Decision for Rising Origin: Resend for sending now; Inbox off on his pack until he wants a mailbox read in the CRM, the same call as Clients. Found while checking: the RESEND_FROM_EMAIL default in code is Ventari's address, and three older send paths in lib/email.ts hardcode info@ventarimarketing.com. With his key and from-address set the default is not used, but those three would send as Ventari from his instance. A small lane before he sends anything from the CRM: the send identity comes from integrations on the pack, the three literals go, and the leak check covers the from-address.

        Exit: the four surfaces on Rising Origin's instance show his identifiers or an honest not-connected state in his name, never Ventari's; his real Search Console, social and mailbox connected by hand from the hand-steps in the reports; the agency unchanged.

        Corrected 2026-09-14: a client pack never uses the platform Resend key (#694). Sending on Connections takes the client's own key.

        Corrected 2026-09-15, X

        The X developer app (client id and secret) is a platform secret, injected on the instance. Alex: those values are on Rising Origin's Vercel project. What remains is Nick authorizing his X user (OAuth on Connections / Social Media). His pack has expectedUsername: null, so any account he connects is his; the agency pack still requires @ventaridigital. A missing expected handle must not refuse the callback (social_x_not_connected).

        Ask Executive, named 2026-09-15 so it is not lost

        Nico, 2026-09-14: ChatGPT and Claude lanes stay in Ventari's CRM; a client CRM is bring-your-own-model only. Already enforced: lib/executive/tenant-model-gate.ts — on a client deploy the Ask Executive launcher is not mounted, and seat-lane env (ANTHROPIC_API_KEY, XAI_API_KEY, Codex/Grok executive, …) is stripped at boot. There is no Connections card for an AI key yet. If a CRM buyer gets an assistant, it is a sixth connection kind on that page, the client's key, their instance, no fallback to Ventari. Credits are theirs. Do not wire Nick to Ventari's Aria. Product question for Nico: does a CRM buyer get Ask Executive, and on whose key. Not on the Nick call.

        Phase 9e - Design roles: what a token pass cannot do

        Full specification textdetail for agents and careful review
        Added 2026-09-13, from the gate-pass lane

        The gate's seven decisions split two ways. Four are colour: lime literals to the accent, status pills, the selected row's edge, the active nav item. Three are design: one solid primary button per card, a mono kicker over a display title, raised rounded cards. The lane applied all seven through tokens and the agency's funnel, CRM and org workspace changed (720,008 differing pixels on the funnel): tokens can swap a value, they cannot make one pack's button solid and another's ghost. The branch is held at hold/gate-pass-2026-09-13, not merged; Ventari's instance is the team's daily tool under D20 and does not change its look without them.

        The fix is the contract, not the lane

        The pack gains a designRoles block: primaryButton: 'solid' | 'ghost', pageHeader: 'kicker' | 'plain', cardSurface: 'raised' | 'flat'. The agency declares its current look; the components read the role; Rising Origin declares his. The held branch is rebased onto that contract and re-run to fourteen zeros. Alex chose this over accepting the change on Ventari (2026-09-13).

        Landed 2026-09-13 evening

        designRoles on the contract (primaryButton, pageHeader per surface, cardSurface, selectedEdge, statusPill); the agency declares ghost / plain (team: kicker) / flat / none / text, its current look, and rendered fourteen zeros; Rising Origin declares solid / kicker / raised / accent / outlined. The held branch was re-done on the contract, not merged as it was.

        Exit: met on the agency side; Alex's second look on Rising Origin's live funnel and CRM after the release that carries it.

        Phase 9f - Meta connections: the client's own app, on the Connections page

        Full specification textdetail for agents and careful review
        Added 2026-09-13

        Ventari's app talks to Facebook and Instagram through long-lived tokens pasted into env (FACEBOOK_PAGE_ACCESS_TOKEN, INSTAGRAM_ACCESS_TOKEN, META_USER_ACCESS_TOKEN), minted from a Meta developer app that is not under a Ventari business because Ventari has no Facebook Business account; the tokens are believed to come from Nico's personal account and expire on a 60-day clock. Setting a client up the same way means Ventari handling the client's tokens every two months. Not the product.

        Two models

        (A) One Ventari-owned Meta app every client authorises: zero-touch for clients, but needs a Ventari Business, Business Verification and App Review for five permissions, weeks of clock, and makes Ventari the tech provider Meta holds responsible. (B) Each client's own Meta app: the client creates a developer app under their own business, enters its App ID and Secret on the Connections page (encrypted, like a mailbox password), clicks Connect and approves on Meta's screen for their own Pages and Instagram. No review, because a business's own app has standard access to that business's own assets. Decided 2026-09-13: B is the product default, and A starts now for Ventari itself. Confirmed by Alex 2026-09-14 after the team call, where he had argued A ("one umbrella, the scalable option"): B gives one-click client-owned connection tomorrow and leaves nothing entangled if a client leaves; A needs a Ventari Business account that does not exist yet, then Business Verification and App Review. So: the ventarimarketing@gmail Facebook Business and developer app get created now regardless, because Ventari's own instance runs on personal tokens today and that is the risk on the table; once that app passes review it becomes the fallback for a client who will not create an app. Nobody waits on it. Nico ratified B on #624.

        The lane

        instance_connections gains kinds meta_app, facebook_page, instagram; a Meta card on Connections in three steps (App ID and Secret with the exact callback URL shown for them to paste into their app; Connect through Facebook Login with the SOP permission set below; pick the Page and its linked Instagram account), a "token expires" line with refresh before 60 days, Disconnect; lib/socials/facebook.ts, instagram.ts and dm.ts resolve stored first, env second, so the agency is unchanged until it connects. Threads later, same pattern. Platform side: nothing, which is the point. Runs after 9e.

        Implemented 2026-09-19 (Phase 9f code lane)

        The additive instance_connections migration carries all three kinds. The card verifies the client-owned App ID and Secret before storing them, takes the optional Facebook Login for Business configuration id, shows the exact callback URL, uses an HTTP-only state cookie for Facebook Login, requires the SOP permission set, exchanges for a long-lived user token, and lists the Pages that token was scoped to. Page selection stores the Page token and its linked Instagram professional account token encrypted in the instance; public status and the browser receive identifiers, names, scopes and expiry only. The existing agency env path remains the fallback. verify:connections and verify:connections-page cover stored-first resolution, no token in status or config, the three-step card, Page selection, the portfolio-Page listing below, config_id over scope, and full disconnect. The live exit below is still open: code proof is not Nick's own-app proof.

        Social Inbox DM credential split (2026-10-06)

        Facebook DM triage uses the encrypted facebook_page connection created by this card. Instagram DM triage must use a separate Instagram Login access token in the client deployment's INSTAGRAM_ACCESS_TOKEN; the instagram row created by Page selection stores the Page token and is used only to identify/account-scope the linked Instagram account. The inbox verifies that graph.instagram.com/me returns the same Instagram identity before listing, triaging, or replying. The Page token is never used for the Instagram Login conversations or reply endpoints. The current Connections card does not issue/store this additional Instagram Login token, so configure it per deployment before enabling client DM triage. Missing or mismatched credentials fail closed and do not hide the Facebook inbox. The separate X triage follow-up binds local triage state to the stored social_x_connections row, verifies the conversation against the live X inbox before state changes, and does not modify provider-side archive or snooze state. The same needs-reply, waiting, snoozed, all, and archived views apply across all three channels. This is human-operated inbox triage, not automated chatbot flow. The shared social_dm_triage pack module is off unless a client pack explicitly enables it; the Rising Origin fixture enables it for its first client preview and the Ventari Agency fixture enables it to preserve its existing Facebook and Instagram Social DMs and host the first X triage test, while other registered packs remain off. The Social DMs tab and its inbox, triage, and reply APIs all check the resolved pack server-side. The email inbox remains available independently.

        Two things the first walk-through found, 2026-09-19 (Rising Origin, Alex driving, signed in as Nico):

        1. /me/accounts can be empty for a Page the user fully controls
          Nico has Full access (Everything) on the Rising Origin Page through the business portfolio, every one of the ten scopes granted, and GET me/accounts returned [] on v21.0 and v26.0. The Access Token Debugger listed the Page (1284116174786165) under pages_show_list granular scopes and the Instagram account (17841477864741917) under every instagram_* scope, and a direct read of the Page returned instagram_business_account and a Page access_token. So the card lists Pages from /me/accounts first and, when that is empty, from debug_token granular scopes (app token, never the user token) read directly. me/businesses needs business_management, which the configuration deliberately does not take.
        2. Meta is retiring the "Other" app-creation path. The dashboard now builds an app from use cases; "Other — this option is going away soon" still exists but a SOP that starts there is dead on arrival. The recipe below is the use-case path, as walked. Meta's docs say config_id "has replaced scope" for Login for Business; the card sends config_id when the configuration id is saved and scope otherwise.

        Exit: Rising Origin posts to his Page and Instagram from the CRM with tokens he minted on his own app, and no Meta credential exists in his Vercel project or in Ventari's hands.

        SOP: client Meta app (model B) — every CRM install

        This is the install recipe, not a one-off for Nick. Walked end to end on 2026-09-19 for Rising Origin; every step below was clicked, and the screens are named as Meta showed them that day. Same steps for Ventari's agency instance (model A app, same permission set, never reused on a client).

        Before you start: three facts to have in hand

        • The Facebook profile that will click Connect. It must (a) have full control of the client's Page and (b) be able to create or administer an app in the client's business portfolio. For Rising Origin that is Nico's personal profile; Nick (info@risingorigin.com) has full portfolio access too. Business-portfolio access and app roles are two different systems; only the second makes an app show up in Graph API Explorer.
        • The Instagram professional account is linked to the Page, not only present in the portfolio. Business Manager → Accounts → Instagram accounts → the account → Connected assets must list the Page. Rising Origin: @risingorigin → Rising Origin Page, confirmed.
        • The contact email is the client's (info@risingorigin.com), never a Ventari mailbox. Meta sends data-use checkups, restriction warnings and policy notices to that address; a Ventari address makes Ventari the responsible party, which is the entanglement model B exists to avoid. It grants no role. Tell the client to forward anything with a deadline.

        Create the app from the client's business portfolio (as the profile above; the app is then business-owned and that profile is admin from the first second — no role invite, no developer registration wait)

        1. Business Manager (business.facebook.com/settings) → Accounts → Apps. Empty means the business owns no app yet; an app created under someone's personal developer account does not appear here and should not be reused — it is a Ventari-held (or one-person-held) credential for the client's assets. (Rising Origin had none: the handoff's "Nick's app" meant his CRM deployment, not a Meta app. Corrected 2026-09-20.)
        2. Add → Create a new app ID. Meta hands off to the developer site with the business pre-selected.
        3. App details: name {client product} CRM (Rising Origin: Rising Origin CRM), contact email the client's.
        4. Use cases. Do not tick "Authenticate and request data from users with Facebook Login" (consumer login: public_profile, email; no pages_* or instagram_*), nothing under Ads, not Threads, not WhatsApp, not "Create an app without a use case", not "Other". Tick exactly:
          • Manage everything on your Page (Content management)
          • Manage messaging & content on Instagram (Content management)
          • Engage with customers on Messenger from Meta (Business messaging)
        5. Business: the client's portfolio (should be pre-selected). Publishing requirements: "No requirements identified" — that is the model-B claim in one line: a business's own app in Development mode needs no App Review for that business's own assets. Overview → Create app.
        6. Back in Business Manager, Accounts → Apps now lists it. That listing is the proof of ownership.

        Dashboard, in this order (the app dashboard at developers.facebook.com, left nav: Dashboard, Required actions, Use cases, Facebook Login for Business, Testing, Publish, App settings, App roles)

        1. Facebook Login for Business → Settings. Client OAuth login yes, Web OAuth login yes, Enforce HTTPS yes, Strict Mode yes. Valid OAuth Redirect URIs — type the URI, press Enter so it becomes a chip, scroll to the bottom, Save changes, then use "Redirect URI to check" below it; it validates the saved list only. Exact, https, no trailing slash: https://rising-crm.vercel.app/api/connections/meta/callback (copy from the Connections card once 9f is on the instance). Preview *.vercel.app hosts are not listed unless someone is testing on one.
        2. Use cases → Manage messaging & content on Instagram → Customize
          Two tabs matter and one is a trap:
          • API setup with Instagram login — not this one. It carries its own Instagram app ID/secret and the instagram_business_* scopes on graph.instagram.com. Do not click "Add all required permissions" there and do not note that ID/secret.
          • API setup with Facebook login — this one. Click Add required content permissions (instagram_basic, instagram_content_publish, pages_read_engagement, pages_show_list, business_management) and Add required messaging permissions (adds instagram_manage_messages).
          • Permissions and features (top of the sub-nav): search manage_comments and add instagram_manage_comments (the row without business in the name); search manage_insights and add instagram_manage_insights. Adding a Page permission here pops "Adding X will affect other use cases on this app" — Add; it is app-wide and the other two use cases legitimately want it.
        3. Use cases → Manage everything on your Page → Customize → Permissions and features. Confirm pages_read_user_content and pages_manage_posts say "Ready for testing"; add them if not. pages_messaging lives on the Messenger use case and is already on. pages_manage_metadata arrives with Messenger (webhooks); leave it as Meta set it. Never add pages_manage_ads, pages_manage_engagement, instagram_manage_engagement, instagram_manage_contents, branded content, shopping, creator marketplace, or anything instagram_business_*.
        4. Facebook Login for Business → Configurations → Create configuration
          Name {client product} CRM. Login variation: standard. Access token: User access token (the Assets step greys out on purpose — the user picks the Page and Instagram account inside the login dialog; that is what the card's select step relies on). Permissions: the ten in the table, and only the ten. This picker only offers permissions the use cases have already added, which is why steps 8–9 come first. Leave business_management and pages_manage_metadata unticked: the card asks for the ten by name and checks for the ten. Create. Note the configuration ID (public; Rising Origin: 1082330371075711) — it goes on the Connections card.
        5. App settings → Basic. App domains: the CRM host only. Privacy Policy URL: on the client's own site, never Ventari's. Category: Business and pages. App ID and App Secret go on Connections. Never into Vercel on a client project. Never into chat.
        6. Leave the app in Development. Live / App Review is not required for a business's own assets.

        This list is META_SCOPES in lib/socials/meta.ts. Keep the configuration, Connect, and this table the same. Meta's summary screen spells one of them instagram_content_publishing; the permission itself is instagram_content_publish — read the Permissions and features row, not the summary.

        Prove it in Graph API Explorer before touching the instance (developers.facebook.com/tools/explorer, as the Page admin)

        1. Meta App: the client's app. User Token. Configuration: the one from step 10. Generate Access Token. In the dialog choose Opt in to current Pages only and tick only the client's Page; same for Instagram. Never "all current and future Pages" — the admin's profile may administer other businesses' Pages and this token must not see them.
        2. me/permissions — all ten granted, nothing else but public_profile.
        3. Tools → Access Token Debugger → Granular Scopes: every pages_* row names the client's Page id, every instagram_* row the Instagram account id. That is the token shape Connect wants.
        4. {page-id}?fields=id,name,instagram_business_account{id,username} — returns the Page with the linked Instagram account. This, not me/accounts, is the proof; me/accounts may be [] (finding 1 above) and the card handles that.
        5. Never paste an Explorer token anywhere: not Vercel, not chat, not a note. Ids and usernames only.

        Then, on the instance (after the 9f release is on the client's host)

        1. Connections → Meta card → App ID, App Secret, configuration ID → Save.
        2. Connect through Facebook as the Page admin; current Pages only; pick the Page; the card stores the Page and Instagram tokens encrypted.
        3. Posting to the Page and Instagram from Socials is the exit.

        Not this configuration, not this card

        What each Explorer symptom means

        "No apps available": the signed-in profile has no role on the app — with a business-owned app created as that profile, this does not happen; with an app someone else created, they must invite and the invitee must accept on developers.facebook.com, not just in the Facebook notification. "No configurations available": step 10 is not done. me/permissions short: steps 8–10 disagree; fix the use cases, then recreate the configuration. me/accounts empty: not on its own a defect (finding 1); check the debugger's granular scopes and the direct Page read. Direct Page read fails: the profile is not a Page admin, or the Page is not in the dialog's selection.

        Phase 9g - Parity with the hand-built CRM: the funnel and org workspace get a client-pack mode

        Full specification textdetail for agents and careful review
        Added 2026-09-13

        Alex on v8: the settings page is perfectly branded; the funnel is still very much Ventari and not completely functional as the initial Rising Origin CRM was. The cutover moved his data onto surfaces built for the agency. The hand-built CRM (18 migrations, 17 components) was small and exactly his. Read against the core, seventeen things (historical inventory on the operating desk; 9g absorbed it, not required for pickup); five are the feeling Alex named, and one has money on it.

        Regressions

        no way to add a deal by hand (the core only takes deals through intake); no lead detail pane (the core funnel's panel is the stage-move confirmation; his F1 pane had Contact, Origin, Profiles, Deal edit, Timeline, Stage history, Remove); the funnel wears the agency's concepts (control room, Motion, Work queues, Stewardship, Automation errors) on a client pack; the org workspace shows the agency's ten tabs instead of his five; the attribution ledger (append-only events, pending / attributed / excluded / disputed, attributable versus excluded source categories, evidence required to decide) did not survive the cutover because the core had no home for it, and it is the mechanism by which Ventari and a client agree which leads Ventari's marketing produced.

        The shape

        not a restyle. 9e makes these surfaces look like his; 9g makes them work like his. Three lanes: 9g-3 the attribution ledger as a pack-gated module (lead_source_adjudication, on for him), a migration fanned out with the next release. Revised 2026-09-13 by Alex: automatic, not manual. The system is the actor: every intake opens an event already classified from the evidence the core holds (Meta lead-ad id or UTMs, booking flow, search referrer, website form, GHL source), attributed or excluded on the spot when definitive, pending only when unclassified; the rule and evidence are stored on the event; re-classification appends when new evidence arrives and never overrides a person; a person only overrides or classifies a hand-made deal. Quiet on the board (a muted source line); the box lives inside the deal and on the org overview. His three decided events re-imported as person-authored and authoritative; 18 of 20 exported events map, the two others belonged to leads refused at cutover. Landed as the ledger lane (see the PR); 9g-1 the funnel for a client pack (New deal, the lead detail pane modelled on the core's affiliate pane, pack- conditional header and controls); landed 2026-09-14: funnel.mode on the contract (agency | client), the agency's tree snapshot-identical, a client pack's header in its words with the four agency controls gone, New deal creating on the pack's pipeline through the intake write path and opening a pending attribution event, the deal pane beside the board (Contact, Deal, Origin, Attribution, Timeline, Stage history, Remove with soft delete); fourteen zeros; 9g-2 the org workspace for a client pack (pack-declared tab set, additional contacts, archive from the existing deleted_at, Audits & Proposals decided with Nick); landed 2026-09-14: orgWorkspace.tabs on the contract, the agency declares its ten, Rising Origin four (overview, messages, pipeline, documents; a fifth, audits_proposals, is one declared id away if Nick wants it), his six overview cards in his order, archive of organisations and contacts that refuses with open deals and never cascades, humanised outlined status pills; fourteen zeros. Order 9g-3, 9g-1, 9g-2. Fourteen zeros on the agency each.

        Exit: Nick adds a deal, opens it, decides its attribution, and works his organisations across his four declared tabs (a fifth, Audits & Proposals, is Nick's call), on the live instance; the agency's funnel and CRM unchanged.

        Phase 9h - Ask Executive for a CRM buyer: the client's own model, on Connections

        Full specification textdetail for agents and careful review

        Scoped 2026-09-20 from the decision in §5.9 (2026-09-19: Cody and Alex; Nico to ratify). Written against lib/executive/tenant-model-gate.ts, lib/executive/provider.ts, lib/agent-connections/host-protocol.ts and the Connections store. No code yet.

        What exists

        Ask Executive is an agency seat. tenant-model-gate makes that a property of the deployment: at boot on a client deploy it deletes every Ventari model credential from process.env (SEAT_LANE_ENV_NAMES: Codex and Grok executive lanes, the gateway key, ANTHROPIC_API_KEY, XAI_API_KEY, the provider pin), so no route or cron on a client instance can spend Ventari's seats or credit. The launcher is not mounted on a client pack (CommandCenterAskExecutive, ConversationPanel). The executive runtime (resolveExecutiveRuntime) picks a provider from env: gateway, anthropic, codex or claude_code on an enrolled host, grok. The agency's own hosts enrol through lib/agent-connections: a staff browser creates an intent, the enrolled host claims it with a one-time nonce, completes provider login on the machine, and echoes the nonce as the canary (host-protocol.ts); the provider's sign-in happens in the native binary and Ventari never sees a session (providers.ts).

        The card. Connections gains kind ai, the sixth card, pack-declared like GHL (pack.integrations.ai.enabled; Rising Origin's pack does not declare it until Nick asks). Two modes, one connection row; the client picks on the card.

        • Key-paste. The client pastes an API key from their own provider account (Anthropic or OpenAI; the card names the provider). Stored as the row's secret, encrypted with the instance key like a mailbox password; identity is the provider plus the key's last four; config holds provider, model, and the verify timestamp. Save verifies the key with one cheap call (models list or a one-token completion) and stores nothing on failure, same as GHL. Server-side, always on, metered on the client's bill.
        • Host-connect. The client's own machine runs the provider CLI signed into the client's own subscription and enrols as a host through the existing host protocol - the same intent → claim → complete → canary sequence the agency's hosts use, with the instance as the broker. The card shows the enrolment code and the host's status (last canary, provider, version). An installer packages the CLI, dependencies and the host agent for macOS and Windows. Works while that machine is awake; that is the trade the client makes for using a subscription instead of a metered key, and the card says so.
        Resolution order, in the executive runtime

        On a client deploy, after the backstop has emptied the seat lanes: stored ai row first (key-paste → the anthropic/gateway-shaped provider on the client's key; host-connect → the enrolled host, as claude_code/codex resolve today), then nothing. There is no env fallback on a client instance - that is the point of the gate, and it stays. On the agency deploy nothing changes: env first, as today. sharedModelAccessAllowed gains one clause: a client tenant with a connected ai row is allowed, spending the client's own credential. The launcher mounts on a client pack when the pack declares the card and the row is connected; otherwise the seat stays hidden.

        What the assistant is on a client instance

        The same executive, with the client's workspace: their CRM, contacts, deals, connections, skills their pack binds (13f). Tools that reach Ventari's own systems (agency vault, Command Center builds, Ventari's Playbooks writes) are not in a client tenant's tool set already; the gate on tools is the existing tenant scoping, which this phase does not loosen.

        The lane

        Additive migration widening instance_connections_kind_check for ai, and a check that a connected ai row with config->>'mode' = 'key' has a secret; lib/connections/types.ts + store.ts (kind, config shape { mode, provider, model, hostNodeId? }), lib/connections/status.ts (aiStatus), lib/connections/resolve.ts (stored only; no env branch); app/api/connections/ai/route.ts (POST save-and-verify, DELETE) and app/api/connections/ai/enrol/route.ts (creates the host intent through lib/agent-connections); components/connections/types.ts, copy.ts (how-to for both modes), ConnectionsWorkspace.tsx (the card, GHL as the template, mode picker); lib/executive/provider.ts (client-row resolution after the backstop) and tenant-model-gate.ts (the one new clause); lib/tenant-packs/types.ts + every pack + contracts/v1/tenant-pack.schema.json (integrations.ai); the launcher mount condition; verifiers: verify:connections (both modes, encrypted at rest, verify-before-store, disconnect clears), verify:connections-page (sixth card, how-to, no provider name from the agency), and the model-gate verifier extended to prove a client instance with no ai row still answers 403 and one with a row spends only the client's credential. Pixel harness on the agency binding: no change expected, the card is pack-gated. §4b and this section in the same PR. The installer is its own lane after the card ships.

        Sequence. Key-paste first - it is the card, the store, the resolver and the gate clause, and it is enough for a client to have an assistant on their own bill the day it ships. Host-connect second, on the existing host protocol; the installer third. Rising Origin's pack declares the card when Nick says he wants it.

        Exit. A client instance whose pack declares the card, with no Ventari model credential in its environment, answers an Ask Executive turn using the credential the client entered on Connections, and the same instance with the row disconnected answers 403. Ventari's own instance is unchanged, proved by the harness.

        Not this card. Ventari-metered usage rebilled at the $1-2K/month passthrough is the Helix tier's model, selected in the customer agreement, and is not a Connections card on a CRM-only instance.

        Built 2026-09-21 (9h.1, key-paste); not yet released or live-proved. What landed, against the scope above:

        • Providers: Anthropic, or the Vercel AI Gateway - not OpenAI direct. The gateway key fronts OpenAI, Google, xAI, Anthropic and Mistral on one key and the client names the model, and the gateway transport already exists; an OpenAI-direct transport does not, and is a later add only if a client refuses the gateway. AiKeyProvider = 'anthropic' | 'gateway'.
        • The row (lib/connections/ai.ts, migration 20260924210000): kind ai; identity = provider; secret = the key, encrypted with the instance key, never returned; config = provider, gateway model, last four of the key. Verified before stored: a 200 on the provider's models list (api.anthropic.com/v1/models with x-api-key; ai-gateway.vercel.sh/v1/models with a bearer); 401/403 refuse; a malformed key never reaches the network; a refused key stores nothing.
        • The card (Assistant): provider select, key (password field), model for the gateway; Connect verifies and stores; Disconnect clears. Copy names the product, never the agency.
        • Resolution (lib/executive/client-model-access.ts, modelAccessForTenant): the agency is exactly the gate that existed. A client instance: pack declares the card + row connected + key readable → the turn's env is the seat-lane-stripped env with one lane laid over it (VENTARI_EXECUTIVE_PROVIDER pinned to anthropic with the key in CLIENT_HEALTH_INTERPRETER_API_KEY, or gateway with AI_GATEWAY_API_KEY and VENTARI_EXECUTIVE_MODEL); anything else → refused with a stripped env. No env fallback; tenant-model-gate and the boot backstop are untouched (the sync gate module did not change).
        • Wired: the two Ask Executive routes (voice/respond, conversations/[id]/replies) and the dashboard layout resolve access through it; on a client the picker's hint (Claude / ChatGPT / Grok - the agency's seats) is ignored and the picker is not rendered; the launcher mounts on a pack that declares the card, so on a client it appears exactly when the card is connected. Every other model-spending route stays agency-only.
        • Pack contract: integrations.ai.enabled required on every pack; Rising Origin declares it (his to connect); the agency, Helix, beeem and Cymatica do not. Rising Origin checksum moved (reason in the pin).
        • Verifier: verify:ai-card (core-stability) - contract, the key check per provider, connect/refuse/disconnect with the key never in config or status, model access in every state with the runtime accepting both overlays, the routes and shell, the card render and copy. verify-tenant-model-gate and verify-command-center-ui pins updated to recognise the resolver.
        • Standard: the three modes for a client's AI - own key (this), own machine (9h.2 - parked 2026-09-21: bespoke, on request, quoted; never a standard offer), Ventari-managed metered (a priced tier, not a card) - are laid out for proposals in the Brain draft client-hosting-and-ai-standard-v1.md, pending Alex and Cody's sign-off.
        9h.1b, built 2026-09-21: the client's assistant reads the client's CRM

        Decided the same night (Alex): the product is "an AI assistant that can read your pipeline and clients" - rung two of the ladder (CRM; CRM + assistant; Helix, the Brain) and never sold as the Brain. What landed, all of it keyed on access.source === 'client_key' so the agency's prompt, tools and lanes are byte-identical before and after:

        • Read tools on the client gateway lane (lib/executive/client-crm-tools.ts): pipeline_overview, find_contacts, list_tasks, upcoming_appointments
          • the instance's own tables through the service client, row-capped and trimmed. The agency's tools (Brain pages, its task board, repositories) never reach a client turn; the provider picks the client set only when the turn says toolSet: client_crm. Anthropic-direct has no tool loop here, so the card recommends a gateway key and says the Anthropic key is conversation only.
        • The client's envelope (buildClientAssistantPrompt): the product's name, the person's role at the product, what the tools can and cannot do (read yes, change nothing yet). Replaces the Command Center envelope wholesale on a client-key turn.
        • Nothing of the agency's in a client's prompt: no canon (the canon's fallback text is Ventari's identity - the co-founder line - so a client-key turn passes canonSections: []), no chief @mentions, no Command Center snapshot, no memory-offer rule; the identity line in the context pack is pack-aware with the agency default intact.
        • verify:ai-card grew to 11 checks covering all of it; the voice-routes and context-pack contracts pass unchanged. Next (9h.1c): write tools
          • create a task, move a deal - through the lifecycle engine, same gating.

        Phase 9i - Native social messaging flows (draft; not launch-ready)

        Full specification textdetail for agents and careful review
        Purpose

        Add customer-facing conversation flows configured per client by Ventari staff and approved by that client in the existing Automations workspace. A business can respond to an eligible social event, deliver a short sequence, branch on replies, wait, stop when a person takes over, and hand a qualified conversation to its CRM. The first proof is a comment-triggered social campaign that moves a person into a short, authored DM experience and a clear CRM handoff. Campaign keywords, copy, media, timing, destination and owner are instance data; they do not belong in shared code.

        Status correction, 2026-10-07

        This correction governs Phase 9i where it conflicts with the 2026-10-04 status sentence and the 2026-10-01 implementation snapshot below. Those paragraphs stay as the record of what was known then. PR #1157 is merged. Current main is 0fed308950ca353459b94941e8ac1cf82399544b. The file maps were read against ac8202ee825647efd13489b785ca5bfb624a8f20, the merge of PR #1219. The only later commit is an intake field-length fix. It does not change social, automation, consent, or CRM Factory docs, so the maps still describe those bytes.

        The maps are pickup evidence, not a second spec:

        Merged, and still default-off or unwired

        Human Facebook, Instagram, and X triage is on main. social_dm_triage is enabled for Rising Origin and Ventari Agency only. Other packs stay off. Native flows are on main and remain off: no inspected pack enables native_flows. The draft helper and the account-scoped draft controller are merged and are not wired into the Social Inbox panel. The donor table below that says Message Triage has no direct first-phase port is historical on that cell. Do not copy local stores, personal credentials, or personal memory.

        Roadmap reassessment, 2026-10-07 (supersedes the "Ordered reuse plan" that stood here)

        Read against main at 37635ad5, with donor SHAs re-verified at their pinned values (messaging-triage 71fff2c1, Codysmessages 32d1a094, Holistic and client repos unchanged). Nothing here approves execution, migration, activation or a provider send.

        Corrections to earlier Phase 9i text (verified in code). (1) The native-flow runtime on main is no longer "one meta_reply plus optional end": it has wait (timer, or until reply with timeout), branch, capture_email, send_email, handoff_crm, stop, per-instance opt-out words, pause, send grants and a runs panel showing provider message ids. The 2026-10-01 and 2026-10-04 "no run receipt, no waits or branches" sentences are historical. (2) What is still missing for flows: no cron entry in vercel.json, no unsubscribe handler (marketing email refuses), staff-reply takeover is set on Facebook only (not Instagram or X), and no pack enables native_flows. (3) SOCIAL_SENDING_ENABLED gates automated sends only; the human reply path is gated by pack module and staff auth. (4) The "no direct port from Message Triage" donor row is superseded by the file-by-file classification below. Licensing and owner authorization are separate questions and are recorded separately under "Cody source reuse". (5) Several donor risks stay in force: Holistic send.ts skips its scope check when scopes are omitted, automation-gate.ts and the Apex lease fail open, Rising CRM suppression ignores query errors, and Holistic keyword precedence is by creation time with an empty list matching every DM. Ventari must fail closed and use explicit priority.

        Shared foundations (one build, used by every capability). Tenant pack gating and per-account isolation, webhook_inbox, the outbox and effect guard, consent and suppression, the Meta 24-hour message and 7-day private-reply windows, per-channel throttling, audit receipts, and the Social Inbox thread model. No capability gets its own store, sender or scheduler.

        Separate gates (shared foundation, different control).

        Draft persistence (open product and architecture choice; recommendation, not a decision). Recommendation: keep persistence out of T1 and make it T1b. Evidence: the only tenant-safe store in this area, social_dm_triage_states (migration 20261005160000_social_dm_triage, extended for X by 20261007030000), is metadata only by its own header: "no message bodies, previews, tokens, or provider payloads". It is keyed by connection, provider account, provider and conversation, has staff-only row level security, and has no tenant column because the deployment database is the instance boundary. It has no draft, text or retention column, and nothing in triage-store.ts deletes or purges rows. A persisted draft is staff-authored text about a customer conversation, so it is a new class of stored content. The in-memory controller already solves the stated T1 problem (an edit surviving a same-analysis refresh) and was verified to have no other caller. If T1b is approved, the work is: (a) a new table beside the triage table, same key plus the analysis token, one row per staff profile and thread, with the same connection foreign key and ON DELETE CASCADE, staff-only RLS and no anon or authenticated direct grants; (b) a text length cap and a stale-analysis rule matching the controller; (c) the retention rule, decided by Alex 2026-10-07: a draft is deleted after 30 days of inactivity, on archive of the thread, on removal of the connection, and after a confirmed send only; a draft is preserved while a send outcome is ambiguous (unconfirmed, timed out or failed without a provider answer), and a failed send never deletes it; (d) deletion on account disconnect, which the cascade covers; (e) a security review confirming draft text is excluded from logs, analytics and any AI prompt until the T5 consent decision; (f) a source migration only, not applied to any hosted database from this change. Alex accepted this on 2026-10-07: T1 keeps drafts in memory and persistence is T1b. T1b implementation is not authorized by this document.

        Cody source reuse: licensing versus owner authorization. These are different questions and the roadmap treats them separately.

        • Verified facts. joyblisscoder/messaging-triage is a private repository owned by the GitHub user account joyblisscoder, the same account that owns VentariFullApp; this host holds admin and push access to it. It has no license file, no notice file and no contributing or use-restriction text in its README or HANDOFF notes. It has one commit (2026-09-16), authored under that account and the Ventari marketing mailbox. joyblisscoder/Codysmessages is private under the same owner, with no license file.
        • What that establishes. A missing license file means no license has been offered to outsiders. It does not show that Ventari lacks permission, because the repository is held in the Ventari-controlled account. Internal reuse needs no license grant from a third party. No restriction on reuse was found.
        • Owner authorization. Alex reported on 2026-10-07 that Cody directly told him to use his existing Message Triage code. That is a direct instruction relayed by Alex, with no separate document, and it is recorded here as the authorization for this reuse work. No separate written confirmation from Cody is required, and nothing in this roadmap is gated on one. The repository itself does not record whether the code is Cody's personal work or Ventari's; the authorization above is the basis for reuse.
        • Separate from licensing, never ported. Personal paths, account identifiers, config.json values, the response profile file, and every message in Codysmessages. These are privacy and tenant-isolation limits, not copyright limits.
        • Third-party code. The donor's dependencies carry their own terms (its README notes a WhatsApp library limited to personal-volume use). Dependencies are not copied, and no part of the WhatsApp path is used.
        • Close reuse is the default where code fits. Every port keeps the donor function name, source SHA and test intent in its header or PR description, and adds tenant keys, fail-closed checks and account scoping.

        File-by-file classification (donor messaging-triage at 71fff2c1; from the 2026-10-07 audit map). Classes: direct port is a near line-for-line translation with only identifiers changed; adapt keeps the logic closely and changes storage, tenancy or safety; rewrite keeps only the behavior; do not use leaves it behind. Line-level fit is confirmed in each slice's PR, not here.

        Other reuse notes unchanged: Holistic additions worth taking are DST-correct send-time scheduling (email/send-time.ts), the self-authored-message skip that prevents reply loops, and the HMAC unsubscribe token pattern. Not found in any source, so new builds: social STOP handling, quiet hours, lead scoring, a template library. Source capabilities the earlier map did not cover (analytics, voice transcription, media-aware drafting, folders and pinning, calendar-link suggestions, contact memory sync) stay deferred.

        Slices and dependencies. Track T is human triage and drafting, track F is automated flows, and the two share foundations but not a release.

        • T1. Wire the draft controller into the Social Inbox with per-account and per-thread isolation, human send unchanged, no AI. Drafts stay in memory (the controller's current design: keyed by account context, provider, conversation and analysis token). A refresh keeps an edited draft; a page reload or sign-out discards it. No migration, no new table, no new stored message content. Safe to start now. It is the first slice of track T, not the whole roadmap.
        • T1b. Persisted drafts across reloads and staff devices. Not part of T1. Needs the decision in "Draft persistence" below, a new migration, and a retention rule. Independent of T2 and T3; T5 and T6 do not require it.
        • T2. Internal needs-reply and waiting-too-long views from Cody's rules. Parallel with T1. No outbound.
        • T3. Delivery receipts and duplicate-send safeguards on the human reply path. Gate for T5 and T6. Parallel with T1 and T2.
        • T4. Confirmed-send-only style learning. Needs T3 and the consent and retention decision.
        • T5. Assisted AI drafting with staff review. Needs T1 and the provider and consent decision. Works without T4.
        • T6. Batch approval and send. Needs T1, T3, T5, throttle and window checks.
        • F1. Flow release readiness: schedule and heartbeat the cron, unsubscribe handler through the email port, takeover on Instagram and X, resolve the Rising Origin tenant literal. Fully parallel with track T, and the longest lead time.
        • F2. Rising Origin comment, private reply, email capture, course link as a saved definition. Needs F1 and Rising Origin setup.
        • F3. Outbound cadences. Needs F1 consent work and T2 rules.
        • Later: analytics; media and voice context.

        MVP. A client-ready first release is T1 plus T2 plus T3 on Rising Origin: the existing live inbox, persisted drafts, a needs-reply queue and receipts. F2 is the second milestone, because it depends on setup that cannot be proved from source. Fuller build: T4 through T6, F3, analytics.

        Shared CRM work versus Rising Origin work. Shared: T1 to T6, F1, F3. Rising Origin only: tenant literal decision and any rename, Meta grants and webhook subscription proof, sending account, activation, one controlled provider test, release and validation. Do not enable native_flows on Rising Origin while its tenant literal is ventari.

        Open decisions for Alex. (1) Tenant literal for Rising Origin: rename, or confirm no collision with agency rows. (2) Whether AI drafting may use client message content, with which provider, and the retention rule. (3) Whether style learning is in scope for Rising Origin at all. (4) Whether risingorigin/rising-crm is the intended repository for the earlier code read. (5) Vercel plan for five-minute cron. (6) Whether outbound cadences are in the first client release or later. (7) Draft persistence: decided 2026-10-07, T1 in memory, T1b later with the retention rule recorded under "Draft persistence". (8) Decided 2026-10-07: Cody told Alex directly to use his Message Triage code, so no written confirmation is required and none gates any port. The privacy limits and the file-by-file port, adapt and rewrite decisions stay in force.

        Checkpoint, 2026-10-07 (late; historical, superseded by the 2026-10-08 checkpoint below). At that time nothing here was merged or deployed. No migration, client activation or live provider test has happened in these steps.

        • T1 (claim #1225), Social Inbox draft wiring. The claim now lists the supporting verifier. Draft PR #1236 is open and cold review is CLEAR. Core Stability, Vercel and Vercel Preview Comments pass; the Booking production gate is skipped. Nothing is merged.
        • T2 (claim #1231), needs-reply and Waiting rules. The claim now links PR #1235, which is ready for human review. Core Stability, Vercel and Vercel Preview Comments pass; Cursor and Netlify remain queued; the Booking production gate is skipped. The PR contains only the rule and inbox-route half, so the visible Waiting view stays on legacy behavior until the Social Inbox panel is wired.
        • F1b (claim #1233) is separately blocked: the claim's file list did not match the required public unsubscribe route, and the worker stopped at a Git ownership guard before editing. It does not block T1 or T2. (Historical as of this 2026-10-07 checkpoint and superseded. Issue #1233 now contains the corrected files and approved decisions. See "Current status, 2026-10-09" for current state.)

        Recommended order from this checkpoint (historical, 2026-10-07; superseded by "Current status, 2026-10-09"; do not follow it. T1, T2 and T2c are complete). (1) Human-review and merge T1 (#1236) after its checks remain green. (2) Finish T2's panel wiring in #1235 after T1 lands, so the inbox uses the five-day Waiting rule and snooze-reopen behavior, then review and merge the complete T2 behavior. (3) Keep migrations, client activation and provider tests on the separate Rising Origin release checklist.

        Checkpoint, 2026-10-08 (historical; verified against GitHub on that date; superseded by "Current status, 2026-10-09" below; it supersedes the late 2026-10-07 checkpoint). No migration, client activation, deployment or live provider test has happened in these steps.

        • T1 (claim #1225), Social Inbox draft wiring: PR #1236 merged, merge commit 5de94fb12c42234812264b577e02865a67bc4826, head 43f50b7786fd1cdebd69529065750df46e470bfc. Drafts stay in memory, keyed by account, provider, conversation and analysis token. Human send path unchanged. Core Stability, Vercel and Vercel Preview Comments passed; the Booking production gate was skipped. No browser session or live provider test was run.
        • T2 (claim #1231), needs-reply and Waiting views: PR #1235 merged as squash commit aaf7702e743fbd4a59fabf71aab03c6a7596c4cf from reviewed head 835ee0e0bb9592c4c3636103d0fc6aebd5cca383. Cold review CLEAR. Core Stability, Vercel and Vercel Preview Comments passed; the Booking gate was skipped. Waiting uses the newest message's own timestamp and a five-day default. GitHub confirms #1231 is closed as completed (2026-10-08); its stale body, which said the pull request was unmerged, was corrected to the merge record. Its three limits are recorded next.
        • T2 limit 1, verifier not registered: scripts/comms/verify-social-triage-views.ts runs only with node --import tsx; it is not an npm script or Core Stability check. The reviewer treated this as a follow-up, not a merge blocker. Needs its own small claim (T2c) that also registers the T1 verifiers.
        • T2 limit 2, no browser or live provider test. This stays on the release checklist, not on shared product work.
        • T2 limit 3, snooze reopen is applied by the inbox route and visible filter only. The single-thread triage GET and POST return the stored status without the thread's messages, so right after a newer inbound they can still say snoozed until the next inbox load. Accepted as a cosmetic limit: the list views are correct and the panel does not drive them from that response. It needs a separate claim only if another consumer starts to rely on that response.
        • F1b (claim #1233, email unsubscribe): still open, unassigned, no branch or PR. No implementation exists: the earlier worker stopped before editing any file because of a Git ownership guard and a mismatch with its prompt. The claim now lists app/unsubscribe/route.ts, lib/automations/flow-crm/consent.ts, lib/automations/flow-crm/email.ts and a verifier, and requires a verified clean worktree before editing. It is independent of track T and does not block it. The assigned worktree has not been verified clean and accessible from the roadmap owner's host, so that check is still required before redispatch. It is kept separate from the #1183 and T3 work.
        • T3 (claim #1232, delivery receipts and duplicate-send guard): queued. Its stated hold is "after T1 and overlap with #1183 resolved". T1 has landed. #1183's own text says the takeover binding shipped in merged #1185 and the issue stays open only for release coordination, so the overlap is now a paperwork item: #1183 has now been narrowed to release coordination and its ownership of lib/socials/dm.ts, app/api/comms/social/reply/route.ts and lib/automations/flow-crm/takeover.ts is explicitly released on GitHub. T3 may claim the reply route and lib/socials/dm.ts under its own claim; #1232's queued hold text should be updated when it is dispatched.

        Where the lane stands, 2026-10-08 (historical; superseded by "Current status, 2026-10-09" below). Merged into main (available in source): human triage on three providers, the draft helper and controller wired into the panel (T1), the needs-reply and Waiting views (T2), and the flow runtime behind the default-off native_flows module. Present but unwired or off: native flows (no pack enables them, no cron entry, no unsubscribe handler), Instagram and X takeover. Still to build: T2c, T3, T1b, T4, T5, T6, F1a to F1d, F2, F3. Needed only for a client rollout: tenant literal, Meta grants, webhook proof, sending account, activation, one controlled provider test. Merged foundations do not mean flows or automated sending are enabled.

        Merge receipt, 2026-10-08 (historical record of the state on that date; items such as #1225 "still open" and #1220 as a draft no longer hold). PR #1239 (this roadmap's lane reassessment) is merged as squash commit 2a44808fe24687b513ed88ab1acab50ed2a177f9 from head b1a49c73fc8f6a38c4c165a714a4bca585b294c7. It changed only docs/crm-factory/SPEC.md and regenerated SPEC.html. Core Stability, spec-guard, Vercel and Vercel Preview Comments passed; the Booking gate was skipped. GitHub state: #1231 closed as completed; #1183 open for release coordination only, with app/api/comms/social/reply/route.ts, lib/socials/dm.ts and lib/automations/flow-crm/takeover.ts released (Facebook-only takeover boundaries kept); #1232 (T3) and #1233 (F1b) open, unassigned, no branch or PR; #1225 (T1 claim) still open although #1236 merged, so its body and state need reconciling. No active native-triage worker; the older social-triage worktree is clean but stale and diverged, so preserve it and start new work from a verified clean checkout of current main.

        Current status, 2026-10-09 (verified against GitHub; this governs wherever it conflicts with the 2026-10-08 notes above, which stay as the dated record). Merged: #1220 (squash e7611e1c), #1230 (7db8f45c), #1235 (T2), #1236 (T1), #1242 (T3a, 85958759), #1244 (T2c verifier registration, 34f52ae6) and #1250 (revised T3 design record, c2128922). Closed: #1225 (T1 claim). T1, T2 and T2c are complete. T3a is merged with social_dm_reply_guard default-off in every pack and no production outbox adapter, so the guarded reply route refuses with 503 and sends nothing through it. T3b (migration, production adapter) has not started. The T3 design record revised 2026-10-08 is direction only; it does not authorize T3b implementation or a migration, and the older outbox-table plan (a separate reply outbox migration) is superseded by it. #1233 (F1b, email unsubscribe) is open and unassigned, with no branch or PR; its decisions and file list are tracked in the claim and in the F1b notes. Nothing in this status authorizes a migration, deployment, provider test, gate change or client activation, and none has happened.

        Next sequence, 2026-10-09 (recommendation, not a dispatch). (1) Settle the documentation PRs (#1240, then #1257); neither is merged without a human decision. (2) F1b (#1233) may be dispatched only after the docs sequence is settled and a fresh clean worktree on current main passes the Git ownership check. (3) T3b waits for an explicit decision on the revised design record; no migration is applied. (4) Deferred, unchanged: T1b persisted drafts; T4 to T6 and F1a to F1d after their gating decisions; T6 after T3 and T5.

        Recommended next sequence, 2026-10-08 (historical and superseded by "Current status, 2026-10-09" below; do not follow it. Its T3 step assumes an outbox migration plan that the revised T3 design record replaces, and it describes #1220 as a draft). Claim-overlap scan on open pull requests and issues, run 2026-10-08: nothing open edits the T3 reply files or the Social Inbox panel. Two things to know. #1220 (draft) edits .github/workflows/core-stability.yml, package.json, the migration runner guards and the baseline list, so it overlaps #1230 on the workflow file and will touch the same migration ledger T3's outbox migration must pass; several other open pull requests also edit package.json.

        1. T3 (#1232), now. First refresh #1232: remove the 'wait for T1 and #1183' hold and the 'owned by #1183' file note, and list the reply route, lib/socials/dm.ts, the receipt display in the Social Inbox panel, a source-only outbox migration and a verifier. Then prepare it from a clean worktree of current main. The migration is written but not applied; applying it is a separate decision. Before the migration is named, check #1220's ledger rules so the file passes the guard.
        2. T2c, needs its own small claim, safe in parallel with T3. It touches package.json and scripts/core-stability-manifest.json, which T3 and F1b do not. It shares no file with #1230. Expect a textual merge conflict in the package.json scripts block with whichever of #1220, #1224, #1203 or #1200 lands first, so it should rebase and register the T1 and T2 verifiers last.
        3. F1b (#1233), separate; hold until the Herdr ownership check passes. Its files (app/unsubscribe/route.ts, consent.ts, email.ts, a verifier) do not overlap T3 or T2c, so it can run in parallel once the worktree is verified clean and accessible on the Herdr side. It has not been verified from this host.
        4. Deferred, unchanged: T1b persisted drafts; T4 to T6 and F1a to F1d after their gating decisions; T6 after T3 and T5.

        What can proceed now / needs an exact decision / release prerequisite (historical, 2026-10-08; superseded). Proceed now (on authorization): refresh #1232 and dispatch T3; open and dispatch a T2c claim; reconcile #1225. Needs a decision from Alex: AI use of client messages (consent, provider, retention; gates T4 and T5); the cron choice (gates F1a); the Rising Origin tenant literal, audit first (gates F2); whether outbound cadences are in the first client release. Release prerequisite only, not shared product work: applying any migration, Meta grants and webhook proof, sending account, activation, one controlled provider test, browser check of the Waiting views. Merged to main is not deployed or enabled for a client; native flows and automated sending stay off unless separately authorized.

        Earlier order from the 2026-10-08 checkpoint (superseded by the next-sequence note above). (1) First, clear the #1183 ownership overlap (done on GitHub; confirm before T3 is dispatched) so T3 can proceed. In parallel, no shared files: T2c verifier registration (touches package.json and scripts/core-stability-manifest.json; draft #1230 changes only .github/workflows/core-stability.yml and adds a separate test step, so the two share no file and there is no dependency or sequencing requirement); F1b unsubscribe (#1233), kept separate and only after its worktree check. (2) After #1183 is narrowed: T3, the gate for T4, T5 and T6. (3) T1b persisted drafts deferred; independent of T3. (4) T5 assisted drafting after the AI-consent decision, T6 batch send after T3 and T5. (5) F1a cron and F1d tenant literal after their decisions. (6) Keep migrations, client activation and provider tests on the separate Rising Origin release checklist.

        Recommended defaults, 2026-10-08 (historical; these were proposals at the time). (a) #1183 narrowing: done on GitHub. (b) Defer T1b persisted drafts. (c) Keep AI use of client messages gated until consent, provider and retention are specified (gates T4 and T5). (d) Defer the cron choice (Vercel versus GitHub Actions) until launch needs it (gates F1a). (e) Audit the Rising Origin tenant literal collision before deciding whether to rename it (gates F2). Nothing here has been migrated, deployed, provider-tested or activated for any client.

        Migration reconciliation, 2026-10-08 (read-only audit of main at merge commit e7611e1c, after #1220; supersedes none of the checkpoints above). No file, baseline, hold marker, database, pull request, deployment, provider connection, gate or client activation was changed by this audit, and no migration was applied.

        • Source inventory. supabase/migrations/ holds 454 files with no duplicate versions and no malformed names. supabase/migrations-baseline.txt lists 453 entries and every entry has a file. The single file not in the baseline is 20261007030000_social_x_dm_triage.sql. That is intentional: the baseline records ordering for files that existed when it was cut, and this file was added after it. It is held, not forgotten.
        • Hold markers. Exactly two files carry -- hosted: hold in their first ten lines: 20261005160000_social_dm_triage.sql and 20261007030000_social_x_dm_triage.sql. Both also carry a "Source migration only. Do not apply it to a hosted database" header, so header and marker agree. Seven other migrations match a looser "not applied / do not apply" text search (work-ledger evidence attribution, approval cards, client repo provisioning, model work admission, model failover tiers, droplet lane slots, client project bindings); none has a hold marker, and this audit did not read or classify them. Whether each is a genuine mismatch is for the human migration review; no marker was normalized.
        • Baseline membership of the social DM file is not an apply record. The baseline records order only.
        • Database status, agency app database only (the database the app's configured URL points to), read through the migration history table with the host's existing read-only access: 453 versions recorded, newest 20261007020000. Source versions with no history row (14): 20260928142800, 20261001120000, 20261001130000, 20261001140000, 20261002090000, 20261002100000, 20261002110000, 20261002120000, 20261002130000, 20261002140000, 20261002150000, 20261004090000, 20261005160000, 20261007030000. History rows with no same-version source file (13): 20260906050829, 20260907011015, 20260907024830, 20260907033212, 20260907212504, 20260915010000, 20260915011000, 20260915185736, 20260919185127, 20260919185133, 20260922163704, 20260928142980, 20261001213426. The two lists are not a clean diff of each other, so history rows alone cannot say which SQL is applied. The object-by-object comparison of the 14 unrecorded source versions has since been completed (see "Object-by-object audit result" below); the 13 sourceless history rows remain unresolved candidates.
        • One ordering anomaly to review: 20261007020000_native_intake_binding.sql (replaces the intake commit function) is recorded while the earlier native-flow chain is not. Its dependency on that chain was not examined.
        • Object-by-object audit result, agency app database only (read-only; catalog metadata and migration history only; no customer or CRM rows were read; compared against main at e7611e1c). Of the 14 source versions with no history row, 2 have matching objects (20260928142800, 20261001140000), 1 is partial (20261004090000: the requested grants are satisfied, but the revoke of INSERT and UPDATE from the authenticated role has not happened), and 11 are absent. Function checks covered name, arguments, rights and one body marker, not full bodies. The 13 history rows without a same-version source file are unresolved candidates, not proven replacements: some have text identical to a source file that has its own history row and look like re-applications under another timestamp, but none of these mappings is proven. The object-by-object audit is therefore no longer the next step. This result does not make the baseline proof of application, does not classify any migration as pending or safe to apply, and does not change either Social DM hold marker.
        • Other databases (client instance databases, Rising Origin, local or disposable): unverified. No authorized read-only access to them was used, and nothing is inferred from the baseline or from the agency database.
        • Social DM triage (20261005160000, and its X extension 20261007030000): intended status is source-only and hosted-held. Verified read-only: social_dm_triage_states does not exist on the agency database, and neither version is in its history, so it is not applied there. Release blockers, all open: (1) the file's own header says there is no tenant column and the deployment database is the instance boundary, so it cannot be applied to a shared database; (2) a tenant-safety review must establish a safe release path (for example per-instance only, or a tenant-scoped key); (3) the X extension depends on the first file and on social_x_connections, and its provider-specific unique keys do not add a tenant; (4) the hold marker may be removed only by a reviewed change. The hold stays.
        • T3 status, revised 2026-10-08: the adapter-only design is insufficient. This supersedes the earlier "adapter over existing tables" reading. The current runtime effect guard does not bind reserve and apply to the worker lease, cannot represent an ambiguous send safely, and has no per-send receipt fields. The reply route, lib/socials/dm.ts, provider send semantics and provider-history confirmation paths have not yet been inspected, so what T3 needs beyond that is not yet established. Existing tenant scoping is unchanged: outbox_events has UNIQUE (tenant_id, topic, idempotency_key) and UNIQUE (tenant_id, id), runtime_effect_guards has UNIQUE (tenant_id, effect_key), and webhook_inbox is keyed (tenant_id, provider, idempotency_key). Any duplicate-send key must include tenant, connection and provider account, and conversation, never conversation alone; no message content is stored in receipts. Any new migration must be source-only and -- hosted: hold, use a <YYYYMMDDHHMM00> version sorting after the newest file, pass verify:migration-order, and be reviewed separately before any apply. Implementation has not started and is not authorized by this note.
        • Smallest safe next step, revised. (1) Review each of the 11 absent, 1 partial and 2 present versions individually, and the 7 other migrations with "not applied / do not apply" wording, which stay unclassified until each is reviewed individually. (2) Read-only inspection of the reply route, lib/socials/dm.ts, provider send semantics and provider-history confirmation paths, to inform a T3 redesign; no T3 implementation until that review is accepted. Do not touch the social DM hold. Everything past that, including any apply, repair, marker change, baseline edit, or client database, needs its own written approval naming exact versions.
        T3 design record, revised 2026-10-08 (read-only review of main at 86859190)

        Direction only. Nothing here authorizes a migration, product code, deployment, provider test, gate change, activation or any change to the Social DM hold markers. It supersedes the "adapter-only" and "not yet inspected" wording above; step (2) of the smallest-safe-next-step is now done as a read, and the redesign below is awaiting acceptance. Preferred direction: extend the existing runtime effect ledger in place (new fenced transition functions and a few columns), not a parallel table. No SQL was run, no database was read, and no provider was called for this record.

        Corrected findings. (a) The job functions (heartbeat, complete, fail) check token, generation, worker, lease_expires_at > now and a running control. The effect functions do not: reserve_effect takes a job id and checks only that the job exists and its control is running; apply_effect reads the job's topic for the control check but never its lease; refuse_effect and compensate_effect check neither. (b) foundation_runtime_enqueue_job inserts a runtime_controls row with the column default running when none exists, and raises when the control is not running. (c) foundation_runtime_claim_job re-claims any processing row whose lease has expired. (d) fail_job schedules a retry (failed, later claimable) and replay_job clones a held row into a new pending job. (e) lib/socials/dm.ts has no request timeout on any provider fetch. (f) lib/socials/reply-outbox.ts stores reply text and treats a similarity of 0.88 or more as proof of delivery; both are rejected below. (g) productionReplyOutbox() still returns null, so the guarded route refuses with 503 and nothing sends through it.

        1. Tenant boundary. Tenant identity for every job, guard and receipt comes from the server-resolved pack's integrations.tenantLiteral, never from the connection row (instance_connections has no tenant column and is unique per kind) and never from request input. The reply route already reads it this way for the Facebook takeover. All five packs on main (agency, Beeem, Cymatica, Helix, Rising Origin) set tenantLiteral: 'ventari', so the literal does not distinguish instances: the database is the instance boundary (one database and one deployment per client, D20). Consequences: (i) every key and uniqueness rule must also bind connection id, provider account, provider and conversation, never conversation alone; (ii) tenant_id equality is a consistency check, not isolation, so no cross-instance read or write is ever acceptable; (iii) before any guarded send the route runs a capability preflight against its own database (required tables, lease columns, effect and transition functions, service-role-only execute rights) and fails closed with 503 if anything is missing, never falling back to an unguarded send; (iv) the guard stays default-off per pack and a pack must not enable it until its own database passes the preflight. Per-instance migration state: unverified. 0065 and 0066 are core, numbered migrations (not agency-only files), but whether they, and any later runtime migration, are recorded and applied in each client instance database is not known; the agency database was audited earlier for the 14 unrecorded versions only, which did not list 0065 or 0066, and it was not re-read for this record. Nothing is inferred from the agency database. Required before any enablement, per instance and read-only, each needing written approval naming that instance: migration-history rows for 0065 and 0066; presence of outbox_events lease columns, runtime_controls, runtime_effect_guards; presence and execute rights of the runtime functions; and the social triage table, which is held.

        2. Human reply job, lease and fenced transitions. The staff request is the only worker. There is no cron route, schedule, allowlist entry or heartbeat for the topic (provisional name social.reply.send), and none may be added. Sequence, each step one transaction that locks the job row, then the guard row, and re-checks token, generation, worker, unexpired lease and control state (except safety transitions, below):

        1. Route, outside any transaction: requireCrmStaff, server-resolved pack, guard flag, capability preflight, live thread verification (existing pre-send reads), and a bounded history read used as diagnostic evidence only. Reject empty text here, before any intent exists.
        2. begin: one function that, in one transaction, requires the topic control to already exist and be running (it must not auto-create a control the way enqueue_job does), then inserts the outbox_events job already processing with a lease, attempts = 1, max_attempts = 1, idempotency key derived from the reply intent id, and inserts the guard reserved bound to that job and its lease generation, with the conversation-scope key (connection, account, provider, conversation). A repeat of the same intent returns the stored state and never claims or reclaims; a live lease returns 409; an expired lease goes to the lease-loss path below.
        3. mark_sending: fenced, reserved to sending, committed before any network I/O. Between steps 2 and 3 the route re-checks takeover and connection validity; a failure there calls refuse (nothing has left).
        4. Provider call with a bounded timeout and abort, sized below the lease so one send needs no heartbeat. The lease length and timeout values are open decisions; no number is proposed here.
        5. Outcome, one fenced transition: provider acceptance with a message id goes to applied (storing provider message id and time) and completes the job in the same transaction; a definitive pre-send refusal goes to refused and the job to cancelled; any other outcome goes to ambiguous and the job to held with a hold_reason. Lease loss and timeout: a fence rejection, crash, timeout or abort never retries and never reclaims. A guard in reserved or sending whose lease has expired is converted to ambiguous and the job held by an expiry transition run on the next touch of that intent or conversation and by an admin sweep; no claim path may select it first. A 2xx with a message id that arrives after the lease expired is recorded as ambiguous with the provider message id kept as evidence (conservative; the operator can confirm sent), not as applied. Principle: transitions toward progress (mark_sending, applied, complete) require a valid lease and a running control; transitions toward safety (ambiguous, hold) require only a matching token and generation, so a control flipped to held mid-send cannot strand a guard in sending. A DB-enforced partial unique index on the conversation-scope key for guards in reserved, sending or ambiguous blocks any new intent on that conversation until the earlier one is resolved; without it, staff could retype and send again past an ambiguous row.

        3. Job-control lifecycle. The control row for the topic is created held by a separate release-controls step before the first possible enqueue, because an enqueue against a missing control would create it running. held is the default and the kill state: while held, begin raises and nothing enqueues. It moves to running only by an explicit, per-instance, written release decision, and back to held or killed at any time. begin runs only while running; progress transitions require running; safety transitions do not. A verifier must assert that no cron, schedule or catalog entry references the topic.

        4. Drain isolation (all callers inspected, not only cron). The generic drainOutbox (lib/integrations/outbox.ts) has two importers on main: the re-export in lib/integrations/index.ts, which nothing imports, and scripts/verify-integration-inbox-outbox.ts, which uses the in-memory store. The only OutboxStore implementation is createMemoryOutboxStore; no database-backed store exists, so the generic drain cannot reach a database row today. The database claim, foundation_runtime_claim_job, takes an explicit job id and is called only by topic-specific workers (automation flows, projection state transition, reviewed-outcome projection) and by crons that filter on one topic. This is a source-level fact, not a guarantee, so the design makes it structural: (i) social-reply jobs are born processing and are never pending or failed; a table CHECK for the topic forbids pending, failed and dead_letter, which also makes fail_job retry and replay_job cloning raise for this topic; (ii) claim_job must exclude the topic family, because the CHECK does not stop a processing row with an expired lease from being re-claimed; this is a reviewed change to a shared function and needs its own disposable-database test; (iii) a repository verifier forbids importing drainOutbox outside tests and requires any future database-backed OutboxStore to exclude reserved topics. Not proven: no SQL was executed, so (i) and (ii) are design requirements, not demonstrated behavior.

        5. Ambiguous sends and the operator flow. Held, never retried: a timeout, abort, crash, thrown network error, empty or unparseable response, a 2xx without a message id, and any received non-2xx. A negative history search never authorizes a resend; a fuzzy text match is not proof of delivery and must not auto-resolve (a reply cannot carry the intent id, so history can only match on text and time). Receipts hold identifiers, state, times and the provider message id, never reply text; the checksum form (plain or keyed) is an open decision. Confirmation flow, exactly one of two actions by an authorized resolver, each a compare-and-swap on the guard still being ambiguous, idempotent, and audited with actor, decision, prior state and evidence counts but no content: (a) confirm sent, which moves the guard to applied and the job to sent, optionally with a provider message id read from fresh verified history; (b) confirm not sent, which moves the guard to a distinct operator-attested refusal and the job to cancelled, and unlocks the conversation. Confirm-not-sent requires a typed acknowledgement that this determination may cause a duplicate message to the customer, a recorded reason, and the screen must show that "no matching message found" is not proof. Neither action sends anything: any new reply is a new intent with a new send. Limitation: because text is not stored, the operator compares provider history with staff recollection or a separate draft, which makes T1b relevant.

        6. Authorization. Provisional resolver role is admin; team may not confirm either outcome, and agency-admin delegation is excluded until its authorization model is verified and approved (not inspected in this pass). Implementation note: requireCrmStaff returns the first of admin or team found in the user's role list, so a user holding both roles can resolve as team depending on list order; the resolver must check that admin is among all of the user's roles, not read the context's single role. The database functions are service-role only and cannot see the user's role, so the role check lives in the route and the audit row records the actor; this is a limit, not a guarantee. Separation of duties (resolver differs from sender) is an open decision.

        7. Provider semantics. Only failures that occur before any request leaves are definitive: SocialDmBoundaryError (including its 502 "could not verify the live thread", which is thrown by the pre-send verification reads and must not be confused with a provider send 5xx) and input validation. Current code mis-types two local failures as plain errors that the guard would hold (empty text, "X is not connected"); they should be rejected before an intent exists or typed as pre-send. Every received non-2xx from Meta or X stays ambiguous until primary provider documentation, read and cited, supports a specific definitive pre-acceptance case; that documentation review has not been done.

        8. Retention. Receipt retention is an open founder decision. No duration is proposed or implied.

        Remaining decisions (none made here). Lease length and request timeout; checksum form; whether a late provider acknowledgement after lease loss may ever auto-confirm; separation of duties; whether confirm-not-sent needs a second approver; receipt retention; per-instance preflight and read-only verification approvals; provider documentation review; and the final topic and state names. Unchanged: both Social DM hold markers, the default-off guard and sending flags, and every release control. Any migration for this design must be source-only and -- hosted: hold, pass verify:migration-order, and be reviewed separately before any apply.

        Merge receipt, 2026-10-08. Docs PR #1248 merged into main as squash commit 86859190e29b75b2ad2238c5883fa0a72a1ce720, pinned to reviewed head c93bf1a276b46d6f7f9f0d904db548caa9771d7a. Core Stability, the spec guard, Vercel and Preview Comments passed; Booking was not listed in the current check rollup, so it is not recorded as passed. The audit's source anchor stays e7611e1c, the commit it examined; the merge commit is the record of when the findings landed, not a new audit anchor. #1248 changed documentation only: no migration was applied, no database or migration history was changed, and no deployment, gate change, provider test or client activation occurred.

        Status as of 2026-10-04 (historical; the 2026-10-07 correction above governs): implemented and locally proven in draft PR #1157; not launch-ready

        The app already has Meta account connections, reads Instagram/Facebook DM threads, can send a human-started DM reply, and has the durable webhook_inbox event ledger. On the unmerged Native branch, tenant-aware Meta intake, event-to-trigger matching, persisted flow runs, an instance-bound sender, an editable meta_reply step, and a bounded outbox worker are implemented. The existing app/api/cron/automation-flows route also calls the native worker locally, but only when a pack enables the default-off native_flows module; no pack currently enables it. There is no client cron allowlist, Vercel schedule, heartbeat or catalog entry for the native worker, and no client-visible run receipt. It currently executes one meta_reply followed by optional end. It checks the exact flow version is still active and set to auto, persists the provider receipt before settling the outbox effect, and holds ambiguous sends rather than retrying. Sending remains default-off behind SOCIAL_SENDING_ENABLED as well. A later local replay applied 424 core migrations, intentionally skipped six agency-scoped files, then applied the run-kind migration as the 425th recorded version. Native/legacy partition checks passed with findings on that disposable database. A forward consent-grant correction is included in the draft PR; its focused disposable-database verifier and independent source review passed, but no hosted migration has been applied. TypeScript and focused runtime, API-source, Meta-source, adapter, integration and run-kind checks passed locally. A safe local HTTP probe returned 401 for missing and incorrect cron credentials; an unknown-host probe was intercepted by middleware before reaching the route and remains inconclusive. No production build, provider send, client-data run or deployment has been exercised for this Native branch. Waits, reply-driven branching, stop/opt-out and takeover behavior, identity-safe CRM handoff, email capture/course delivery, client setup and end-to-end provider proof remain incomplete. The separate CRM rules builder is not this flow runtime. Do not describe the feature as client-ready or merely switched off. The 2026-10-04 reading said PR #1157 was unmerged; the 2026-10-07 correction above replaces that claim. No client deployment or live campaign has been verified.

        Reuse before adding components

        The cross-repository audit found a closer donor than Message Triage: the private Holistic University app already has signed Instagram/Facebook webhook intake, account-scoped keyword comment-to-private-DM and DM-auto-reply rules, provider-window checks, rate-limits, send reservations, ambiguity holds, and a specialized comment → email-capture → lesson-link campaign. Its social implementation is the primary donor for Meta event normalization, trigger matching, send guards, and the first Rising Origin campaign. Treat it as source code to adapt, not proof that the feature is currently enabled or production-ready: the code has explicit feature gates, the admin repo may lag the app, migrations and account setup still need verification. A separate HighLevel workflow audit, not the Holistic social implementation, documents the first-reply/contact-merge edge case.

        Holistic also has a durable, versioned workflow runtime with delays, bounded conditions, tags, email/Meta steps, leases, retries, run receipts, and reconciliation holds. Reuse its lifecycle and recovery patterns selectively. Its generic producer currently requires a resolved CRM contact before enrollment. Its generic workflow capability exposes no active Meta account in production.ts (metaAccounts is an empty set); hu-admin uses separate activation-readiness logic. These limits mean it cannot be copied unchanged for a comment trigger that must reply before CRM identity resolution. Keep one Ventari runtime and adapt the donor behind Ventari's tenant, account, permission, outbox and identity services.

        Message Triage is Cody's personal local inbox: useful patterns include response-style learning from confirmed-sent messages, human-reviewed drafts, thread context, throttling, and local outbox/double-send protection. It does not provide client-scoped webhooks, unattended campaign execution, CRM identity handoff, or a deployable flow builder. The separate Codysmessages archive is not a flow runtime. Do not copy personal message stores, credentials, identifiers, or memory into the CRM.

        The recent ApexRx CRM has a durable When/Then/If CRM automation engine and email sequence jobs. A prior audit reports lead/stage email sequences and send suppression in risingorigin/rising-crm, but the named joyblisscoder/rising-crm URL returned 404 and the repository owner/path did not match. The Herdr operator confirms that worker 01 read code-only files from the alternate repository; no customer or production data was accessed. Treat those findings as provisional until the Brain confirms that this is the intended repository and accepts the out-of-scope code read. Neither repository is known to supply social comment/DM triggers or a customer-facing DM campaign. Community chat in other client apps is user-to-user messaging, not autonomous marketing automation. No inspected client repo closes the end-to-end gap by itself.

        Within Ventari, extend the existing tenant pack and encrypted Meta connection model from Phase 9f; use webhook_inbox for verified, deduplicated provider events, a dedicated automation_flow_runs row as authoritative runtime state, the existing automation_runs ledger as the human-visible projection, and the existing outbox/retry patterns for provider calls. Do not add an event ledger unless implementation review proves the two existing records cannot support required history. Do not create a second CRM, inbox, credential store, or generic scheduler. A comment-triggered flow must start before its sender is a CRM person or opportunity; the existing CRM rules runner is for effects after identity-safe handoff, not a prerequisite for the first reply. Persist a versioned flow snapshot and execution cursor so an instance-owned definition resumes safely. The Automations tab is the authoring surface and must publish a saved, inspectable version the runtime can execute, rather than treating AI prose or catalog steps as executable instructions.

        Donor-to-Ventari port map (audit 2026-10-01). Port behavior and safety invariants, not Holistic's tenant tables or account-selection assumptions. The donor's component boundaries map to Ventari as follows:

        Implementation status (2026-10-01, historical branch snapshot; the 2026-10-07 correction governs)

        This paragraph describes the 2026-10-01 branch, not current main. Tenant-aware Meta intake, event-to-trigger matching, persisted flow runs, the instance-bound sender, an editable meta_reply step and a bounded outbox worker are implemented on the current build branch. The worker is deliberately not on a client cron allowlist or schedule yet, and has no client-visible run receipt; this branch does not make it launch-ready. It executes exactly one meta_reply followed by optional end, checks that the exact flow version is still active and set to auto, and persists the provider receipt before settling the outbox effect. Ambiguous sends are held rather than retried.

        Remaining implementation order

        First package the worker for opted-in client instances, expose run receipts and held-send reconciliation in Automations, then add waits, bounded reply branches, opt-out/human takeover, safe CRM identity linking and handoff, and the Rising Origin email-capture/course-link campaign as a saved definition. Use one worker per client instance, never one per flow. The recommended Rising Origin cadence is every five minutes on Vercel Pro, subject to confirming the project's plan and cost. Vercel Hobby supports only daily cron schedules; if the project remains on Hobby, do not promise sub-daily waits. Supabase Cron is an alternative only if the Brain review confirms it fits the deployment and operations model. Keep each release to the smallest step set a real client flow needs. Before activation, verify each pack's OAuth grants and the selected account's token status. The adapter checks pages_messaging for Facebook private replies; Meta's Messenger Send API identifies that as the sending permission. Keep Phase 9f's prohibition on pages_manage_engagement, which is a broader Page comment-management permission and is not needed for this private-message send. The comment intake path still needs a separate end-to-end check against the existing webhook subscription and comment-read grants. The Automations editor now has a saved meta_reply step, and a bounded worker hands eligible sends to the existing durable outbox/effect guards. It only executes a single meta_reply followed by optional end; the current definition must still be active, unchanged, and set to auto. Ambiguous sends are held for reconciliation and never retried automatically. Sending remains disabled unless the instance's explicit SOCIAL_SENDING_ENABLED gate is enabled. No donor files have been copied into Ventari; the sender behavior has been adapted into a new Ventari module.

        First build boundary. Implement deterministic authored steps first:

        1. Trigger on an eligible Instagram or Facebook comment, or a new inbound DM; support an explicit keyword/phrase match and account/media scope.
        2. Send an approved Instagram comment private reply or eligible inbound DM through the existing connected Meta account; record the provider receipt and make retries idempotent. The first executable flow shape is one saved meta_reply step followed by optional end. Facebook inbound-DM replies and comment private replies require pages_messaging and their respective provider windows; comment event intake and read permissions must also be proved. The worker route exists but is not on a client cron allowlist or schedule, so do not promise automatic or delayed execution yet.
        3. Wait for a configured interval and continue from durable state. A client instance may ship with no cron routes, so the build must define and monitor one pack-allowlisted delayed-step worker before promising waits. Never add a cron per flow.
        4. Branch only on explicit, bounded reply conditions in the first release. Stop automation on opt-out, a human reply/takeover, provider-window or permission failure, or a kill switch; surface the reason in Runs.
        5. Resolve the social sender to a CRM person without silently merging people. Create or update an opportunity only through existing identity-safe CRM services. Record a named owner and handoff task when the configured goal is reached.
        Autonomous sending is disabled by default

        A client flow may send only after the instance has an explicit, revocable authorization for that channel and flow, a connected client-owned account, approved content and an available provider permission/window. The agency pack continues to follow the existing no-automated-outbound decision. A flow's review/auto setting cannot override these gates. No generative, open-ended responder is in the first build; AI may help author drafts, but only saved and validated steps execute.

        Parallel execution plan (reviewed 2026-10-01)

        Use the Herdr Agent Operator Workflow with one operator responsible for the contract, task ownership, integration, and release gates. Protect the existing feature-branch changes; workers must record the baseline and may not reset, clean, overwrite, or rebase the checkout. Work prompts and reports live in the active lane's operator session folder (outside this repository), not in this tracked spec.

        1. Read-only review wave (parallel)
          (a) cold-check the existing donor coverage and port map, focusing only on unresolved Holistic runtime behavior and source claims rather than repeating the completed repo sweep; (b) trace the current Ventari branch from Meta event to trigger, flow run, sender and outbox, including what the code cannot prove about live provider setup; (c) map the Rising Origin journey to existing consent/email/identity/CRM services and list decisions still owned by Cody/client; (d) audit client capability installation, scheduling/hosting, Automations UI and run operations. Each worker reports exact source paths, verified behavior, unknowns and recommended reuse; no product edits.
        2. Owner review and preservation gate: On 2026-10-02 Alex accepted Codex's internal review in place of waiting for Brain credits. Brain review remains welcome and may revise these defaults, but is not a dispatch blocker. Keep the risingorigin/rising-crm findings excluded from implementation decisions unless their owner/path is confirmed. Do not dispatch implementation workers until the uncommitted foundation has a verified preservation checkpoint and Alex has authorized the worker wave.
        3. Interface freeze (operator): use the adopted defaults below as the baseline, then translate them into the typed, versioned flow contract, run/event/outbox receipt relationship, consent/identity/handoff interfaces, per-instance activation gates, and exclusive file ownership. Reopen a default only when source evidence or implementation constraints require a change, and record the reason for Brain review. Do not begin parallel implementation before this contract is stable.
        4. Implementation wave (parallel only on disjoint ownership): (a) bounded durable flow runtime; (b) Meta event and delivery adapters; (c) Automations authoring, validation, preview and run operations; (d) adapters to existing CRM identity, consent/suppression and email delivery; (e) dormant per-client installation, schedule and health/runbook. Existing inbox, connection, outbox/effect guards, identity, email, and scheduler infrastructure take precedence over adding parallel systems. Shared schema, migration, or contract changes remain operator-owned or serialized.
        5. Integration and independent review: operator integrates without losing pre-existing changes, updates Markdown and its generated HTML together, and records implemented/partial/unverified/deferred status. Parallel reviewers separately inspect safety/tenant isolation, runtime/recovery behavior, and product/setup/rollback completeness. Fixes have one named owner at a time.
        6. Review and launch gates: provide source-reuse map, architecture and node contract, Rising Origin example, supported per-instance capability matrix, test evidence, known limits, and explicit client decisions. Run the complete journey on an approved test account. Production sending stays disabled until exact content, timing, consent, channel authorization, owner/SLA, and immutable flow version are approved. Review readiness is not deployment or activation approval.

        The operator prompt, numbered worker prompts, report paths, allowed file ownership, and verification commands must agree with this plan before agents are dispatched. Read-only discovery may fan out immediately; implementation workers wait for the interface-freeze gate. Do not use hidden in-session subagents in place of visible Herdr worker panes.

        Per-instance setup SOP (once the feature ships).

        1. Confirm the client owns/administers the Meta app, Page and linked professional Instagram account, following Phase 9f's model-B procedure. Never connect a client's account to Ventari's agency app.
        2. Connect the Page and Instagram account on the instance's Meta card. Set the instance's META_WEBHOOK_VERIFY_TOKEN, callback URL and app secret; configure the webhook fields and permissions required for message and comment events. Confirm the deployed release exposes the callback and Meta verifies it. Check the required permission/field list against Meta's current official docs before each setup; Phase 9f's existing publishing and inbox scope list alone is not proof of webhook subscription.
        3. Send a signed provider test event and confirm it is accepted once, stored under the instance tenant, deduplicated on replay, and rejected for a different account or invalid signature. Confirm the event reaches a flow preview without sending a customer message.
        4. Configure and save the flow: exact trigger and keyword, account/media scope, approved messages/media, waits, reply branches, stop/opt-out rules, CRM goal, human owner and handoff SLA. All values are specific to that client and live in its instance data.
        5. Run preview and delivery-path checks, resolve permission, identity, schedule and provider-window holds, then obtain the client's explicit approval of the published flow and send authorization. Activate only that version. Confirm kill switch, event receipts, failed-send visibility and human handoff before declaring the campaign live.
        Launch acceptance

        Synthetic checks prove signed comment and DM intake, account isolation, retry dedupe, keyword matching, private reply, delayed continuation, reply branching, human takeover/stop, CRM identity and handoff, provider failure recovery, run history and immediate kill-switch behavior. Then a client-approved test account proves the provider path end to end. A successful webhook receipt alone is not proof of a working flow. Keep the first production campaign inactive until its exact content, offer, timing, media, CTA, send authorization and CRM handoff owner are approved. Adopted architecture defaults (Alex, 2026-10-02; internal review). These are the v1 implementation defaults. Brain may review and recommend changes later; that review is not a blocker to the approved build-preparation phase. These defaults do not approve production activation.

        • Flow contract: immutable, validated flow.v1 definitions with explicit comment/DM triggers and typed meta_reply, wait, bounded branch, capture_email, send_email, handoff_crm, stop and end steps. Limit a flow to 12 steps and a total wait horizon of 7 days. Reject cycles and open-ended AI responders. A published version hash pins each run.
        • Matching: exact, whole-phrase and contains modes use Unicode and case normalization. Whole phrase is the default. Preserve each client's trigger values, apply explicit priority, and run only the highest-priority matching flow for each event.
        • Receipts: automation_flow_runs is authoritative for runtime state and cursor; existing automation_runs is the operator-visible run history and evidence projection. Provider events remain in webhook_inbox, and sends link to existing outbox/effect guards. Add no separate event ledger unless technical review demonstrates a specific missing requirement.
        • Roles and send approval: Ventari staff author and publish v1 flows. The client reviews and approves the exact content and grants or revokes a per-flow send authorization. Client controls include view, pause and stop; client editing can follow once the role model supports it. A flow cannot grant itself permission to send.
        • Capability gate: one default-off native_flows capability, enforced server-side on API, webhook and worker paths. Hiding navigation is not authorization. Keep agency outbound disabled and retain SOCIAL_SENDING_ENABLED as an outer kill switch.
        • Scheduling: one pack-allowlisted worker per client instance with a monitored heartbeat; no worker per flow. Recommend a five-minute Vercel Pro schedule for Rising Origin only after confirming plan and cost. Hobby allows daily schedules only, so it cannot support promised sub-daily waits. See Vercel Cron usage and pricing and Supabase Cron. Supabase Cron remains a fallback for Brain review if it better fits operations.
        • Safety and recovery: fail closed on tenant/account/scope/window, authorization, consent and suppression checks. Hold ambiguous sends for an explicit, audited staff resolution; never blindly replay them. Persist opt-outs and human takeovers and stop the affected run.
        • Identity and email
          retain a provider-scoped provisional identity until email is supplied; treat captured email as unverified and never auto-merge on it. Route collisions to review. Use a durable consent reader/writer against the existing consent schema if it supports the required policy; otherwise extend that consent service rather than adding a parallel ledger. Fail closed, keep requested course delivery consent distinct from ongoing marketing consent, and honor durable STOP/unsubscribe suppression. Do not verify an email solely to deliver a specifically requested course link after consent and suppression checks; require verified ownership or client-approved review before associating it with an existing CRM identity.
        • First campaign: Rising Origin's first flow is the ad-comment-triggered mini-course campaign, on one client-owned social account with one approved comment trigger and authored private reply. Add email capture and course-link delivery after Rising Origin supplies approved copy, destination URL, consent language and a named owner. The success goal is course requested and delivered plus a CRM handoff task; do not assume booked call as the goal.
        • Evidence boundary: the risingorigin/rising-crm findings remain provisional because the named URL returned 404 and the alternate owner/path needs Brain confirmation. The operator records a code-only read and no customer/production-data access. This does not authorize reads of other unlisted client repositories. Preserve the dirty foundation branch untouched; checkpoint it on its own branch before creating separate implementation worktrees.
        Remaining client inputs and live proof

        Rising Origin still needs to provide its exact platform and account, keyword, approved copy/media/course URL, timing, consent wording, and named owner/SLA. Verify current Meta grants, webhook subscriptions, is_echo behavior during staff takeover, provider send windows, and a complete test-account journey. These are setup or evidence gates, not unresolved architecture choices. Keep production sending disabled until the client approves exact content, timing, consent, channel authorization, owner/SLA and immutable flow version. Brain review may revise any adopted default before implementation proceeds.

        Source corrections from the 2026-10-01 review. The Facebook donor scope list contains pages_manage_engagement, which Phase 9f prohibits; do not port it. The migration-scope verifier already exists at scripts/migrations/verify-agency-scope.ts and is run as npm run verify:migrations-agency-scope.

        Evidence and working reports (operator-local)

        The reports below are working evidence in the operator's session folder. They are outside this repository, so GitHub readers must request the report packet from the operator for a source audit. The decisions, port boundaries and remaining proof gates needed to work from this spec are recorded above; these names are references rather than repository links. The reports do not prove a live provider or production deployment:

        • crm-factory-native-chatbot-2026-09-30/reports/03-repository-coverage.md
        • crm-factory-native-chatbot-2026-09-30/reports/02-crm-native-implementation-map.md
        • crm-factory-native-chatbot-2026-09-30/reports/01-source-access-and-capability-audit.md
        • crm-native-automation-build-2026-10-01/reports/01-donor-evidence.md
        • crm-native-automation-build-2026-10-01/reports/02-ventari-runtime.md
        • crm-native-automation-build-2026-10-01/reports/03-rising-origin-journey.md
        • crm-native-automation-build-2026-10-01/reports/04-instance-operations.md
        • crm-native-automation-build-2026-10-01/reports/05-interface-freeze-proposal.md

        Phase 10 - Pack registration without a code change

        Full specification textdetail for agents and careful review

        The one that changes what onboarding is. While REGISTERED_PACKS is a compiled array, every client is a release of the shared codebase, nobody can be onboarded during a code freeze, and client five is a merge conflict in the same file as client four.

        • A pack registry the application reads at runtime rather than compiles.
        • A binding that points at an unknown pack must fail loudly, never silently drop. That is today's behaviour and it is how a misconfiguration becomes invisible.
        • The unbound-host finding is decided here or explicitly deferred again. A host with no binding currently resolves to the agency pack and renders Ventari's chrome. verify:client-surface-leak:unbound already asserts it and exits 1.
        Landed 2026-09-14

        unknown hosts fail closed. resolveTenantContext returns status unbound with no pack. Middleware rewrites to a plain not-configured page (404) before auth. /api/host-binding reports { status: 'unbound', host }. localhost / 127.0.0.1 still resolve to the agency pack when NODE_ENV !== 'production'. Named Ventari hosts stay bound. Before shipping to a Vercel project, TENANT_PACK_HOST_BINDINGS must name every host that project serves, including *.vercel.app preview hostnames.

        Exit: a new client can be registered and bound without a merge, and verify:client-surface-leak still passes against the new registry.

        Watch for: the leak check asserts that every pack registered in tenant-context appears in its own table. A runtime registry must keep that assertion meaningful, or the check quietly stops covering new clients.

        Phase 11 - Snapshot provisioning

        How a new client database starts clean. Chosen over sorting 279 migrations because the sort is a week and the snapshot is days, and because a forward cleanup migration cannot tell a Ventari database from a client one, which makes it a delete-live-data risk. See estate map SPEC 1.5, section 8.7 item 4.

        • One schema-only baseline that a new client database starts from.
        • Acceptance is Phase 9's count. The number of Ventari rows removed by hand must be zero when a database is built from the snapshot.
        • A check that fails if the snapshot drifts from what the migration history produces. Without it the snapshot rots and nobody notices until a client install is subtly wrong.

        Exit: a fresh client database contains zero Ventari organisations, zero Ventari price book entries, and passes the same checks Ventari's own does.

        Known cost, accepted: a snapshot is a second artifact to maintain. That is the price of not auditing 107 seed statements.

        Phase 12 - Migration fan-out

        Full specification textdetail for agents and careful review

        How a schema change reaches every client. Code propagates on deploy. Schema does not.

        • A way to apply a migration across every client database.
        • It reports per database: applied, skipped, failed, and current version. A fan-out that cannot tell you which clients are behind is not a fan-out.
        • Drift is visible without being asked for. A client behind the application's expected version should be discoverable before a user finds it.

        Exit: a schema change can be applied to every client database in one operation, and the current version of every client is knowable at any time.

        First real run, 2026-09-13 to 14, and what it settled about accounts

        scripts/apply-migrations.mjs applied 32 pending migrations to Rising Origin's project in order through the Supabase Management API (local rehearsal stack first), and reports per database: applied count, pending list, verify. Ventari's own production database lives in a different Supabase account, info@ventarimarketing.com, which the operator's first Management API token did not cover; that is why it was invisible from the operator's seat and why ventari-atitlan (a project in another account) was briefly mistaken for it. Alex has direct access to the info@ organisation; the fix is a second token minted there. The fan-out order stays local, then Ventari's database, then clients, and the mechanism is the same script with a token from the right account. Resolved 2026-09-14: the operator holds one token per Supabase account (SUPABASE_PAT for the client org, SUPABASE_PAT_VENTARI for the info@ org) and runs the same script against Ventari's database first, every time. A release that carries a migration reaches a client only after that migration has run on Ventari's database; the one exception was v9 (2026-09-13), proved on the local copy of the client's own schema while the account question was open.

        Two things the first run found that the runbook now carries

        A client whose legacy schema shares object names with a later core migration (his old attribution ledger's table, views, indexes and enum types) collides at fan-out time even though the cutover renamed only the tables that collided then; the fix is the same ro_legacy_ rename, applied to every object kind, before the migration runs (one-off on the operating desk; not required for pickup). And a migration applied on a stack by a lane worker without a history row makes the fan-out believe it is pending; the worker's report must show the history row or the operator inserts it.

        This is the phase that makes the one-product model real. Without it, "we maintain one product" is true of the code and false of the data.

        Landed 2026-09-14

        public.instance_identity is one row naming the pack this database belongs to. The migration creates the table empty; claiming is a deliberate command (scripts/claim-instance-identity.ts), never automatic in a migration. Fan-out --expect-pack, the deploy action, and startup all refuse a mismatch and name both ids. Unclaimed is allowed until every instance is claimed. Ventari's database is claimed first.

        Corrected 2026-09-14, after v16 refused to ship

        the deploy action reads the client's service-role key from vercel env pull, and a key stored Sensitive on the Vercel project (which is where a service-role key belongs) pulls as the placeholder [SENSITIVE]. The runner therefore often cannot read the row at all. Rule: the deploy preflight refuses only on a real mismatch; a key it cannot read, a rejected key, or an unreachable database is unverified, a warning on the job, and the deploy proceeds, because startup and the fan-out check the same row with the real key. STRICT_INSTANCE_IDENTITY=1 on the workflow turns unverified into a refusal once every instance has a readable key on the runner.

        Second correction, same night (v17 built with no database URL)

        the assertions' env pull had replaced, and the pre-build cleanup then deleted, the CLI's own .vercel/.env.production.local, which vercel build reads and from which every NEXT_PUBLIC_* value is inlined. Rule: the build reads the CLI's file, untouched; the assertions pull to a separate file that is removed before the build. The smoke's unbound-host probe expects a 404 and no Ventari surface; Vercel answers a made-up host with its own 404 before the app runs, so the app's not-configured page is not something the smoke can demand.

        Third correction, same night (#703 CI red on client-deploy-smoke): verify-client-deploy-action.ts pinned the new probe; verify-client-deploy-smoke.ts still demanded the old body string in the workflow. Both verifiers pin the same contract: 404, no Ventari surface, not the app's not-configured page.

        A core migration carries no agency data. Rule, 2026-09-20, after the first client fan-out since 09-15

        Rising Origin had 34 pending. A plain apply ran 21 in order and halted on mr_pams_owner_approved_agent_team (an inline assertion that Ventari's client exists); beeem_paid_client would have hit a foreign key there and had already failed on Ventari's own database (its direct deal-stage UPDATE is what the pipeline lifecycle trigger exists to refuse). Roster rows, payments and deal state for the agency's own clients had been written as core migrations, and a client instance is exactly the database where those rows do not exist. Three mechanics now hold the line:

        • -- scope: agency in a migration's first ten lines marks agency data. scripts/apply-migrations.mjs skips it on a client target (--expect-pack other than the agency pack), reports it as skipped, never pending, never applied, and refuses --only on it. A scoped migration carries no DDL, because a skipped migration must not leave a client schema behind; a migration that needs both is split, or its data half gains a no-op path (RETURN when the organisation is absent) and stays core.
        • verify:migrations-agency-scope (core-stability) fails a migration from version 20260922000000 on that writes a specific organisation at top level without the header or a WHERE EXISTS, or whose DO block asserts one with no RETURN, or that is scoped and carries DDL. Mutation-tested on all three. Older migrations are reported as residue: client_roster_truth and client_roster_correction (09-14) are the same pattern and reached Rising Origin only because his organisation set happened to satisfy them.
        • The deal-stage move is not a migration's to make. The lifecycle function wants an actor, a reason and the expected revision - evidence a migration cannot honestly supply. Roster and deal changes for the agency's clients are made in the app, under a name. beeem_paid_client was rewritten on this rule before its first hosted run.
        The release does not run ahead of the migration

        Same night: v27 shipped the 9f code before the 9f migration had reached Ventari's database or his; the action asserted host and identity but not migration state. Now /api/host-binding reports the instance's highest applied migration (instance_latest_migration(), service-role, definer), the ship job computes the highest core version the release carries (scoped ones excluded), and the smoke fails on a database behind the release, naming both versions. An instance that predates the reporting function is noted, not failed, for that one release. The order stays: local, Ventari's database, the client's database, then the tag.

        Phase 13 - The install runbook, automated

        Full specification textdetail for agents and careful review

        Only now is there something worth automating, because Phases 9 to 12 have made each step real and measured.

        • Rows 3 to 6 become repeatable: database project, deployment, env and host bindings, domain.
        • Whatever cannot be automated is written down as a named manual step with an owner, rather than left implicit.

        13a - Client release transport. CLI standard adopted 2026-09-27.

        The procedure, with exact commands, lives in one place

        docs/crm-factory/runbooks/cutover.md → "Release a client version" (added 2026-09-21 from v30). Migrations first, then one exact merged source SHA, a Git-free and client-pruned package, a remotely built Vercel preview, smoke verification, and separately approved CLI promotion. The token location and the Supabase role it needs are on that page.

        The code and the instance are two different things

        The code is one repository. An instance is a Vercel project pointed at it plus three settings that make it a client's: their database keys, their host binding, their domain. Nick's current CRM already lives in his own Vercel project, so the question was how to put the new instance there without connecting his project to Ventari's private repository.

        Current answer: an operator uses the Vercel CLI, and Vercel remotely builds the uploaded source inside the client's existing project. The client's project never has a Git integration

        The operator may work from Mac, Windows or Linux. The client sees deployments, logs, domains and billing in their own dashboard. Access remains revocable at the Vercel account or token boundary; the private repository is not connected to the client project. The prior CI and prebuilt workflow remains documented below as parked future infrastructure, not the active release path.

        The current release, in order

        fan-out first, Ventari's database then the client's, until plan shows only scoped migrations skipped; package the exact merged SHA without Git metadata; apply the client's pruning; upload source by CLI for a remote preview build; smoke the generated preview; separately approve promotion; wait for the production rebuild; smoke the canonical host; record the receipt. A local Windows prebuilt is specifically prohibited by the 2026-09-27 missing-chunk failure. Running the Vercel CLI on Windows is supported.

        Parked action history (2026-09-20, after v25-v28 on Rising Origin). The GitHub workflow below previously dry-ran a tag, published a Release, waited on the environment gate, shipped and smoked. Its rules remain recorded for later versioning-system review; they are not current operator instructions.

        Rules the action carries:

        • Triggers on a stable release tag per pack, never on a push to main. Legacy production tags retain client/<packId>/v<n>; new stable tags use client/<packId>/v<major>.<minor>.<patch>. A client instance moves when Ventari publishes a stable GitHub Release and clears the existing client environment approval, not on every merge.
        • One pack, one project. The workflow maps packId to the three identifiers (VERCEL_ORG_ID, VERCEL_PROJECT_ID, token secret name) from a checked-in allowlist; a tag for a pack not in the list fails before it builds.
        • The host binding is asserted, not assumed. Before deploy, the action reads the target project's TENANT_PACK_HOST_BINDINGS and refuses if it does not bind to the tagged pack. Deploying Ventari's pack into a client's project is the failure this exists to prevent.
        • The client's existing project, not a new one. Revised the same day: Alex did not want to ask Nick for a new project. A Vercel project holds many deployments and one production alias, so the demo is a preview deployment into his existing project (its own URL, his domain untouched), the migration is the same deploy promoted to production (his domain follows), and the rollback is Vercel's promote-previous-deployment. The one hand step at cutover: disconnect the old repository's Git integration from that project, or a push to the old CRM would redeploy over the new.
        • Secrets never enter a worker lane. The workflow and its checks are built and proved with a dry run that builds and prints what would be uploaded; the first real deploy is Alex and the operator with the token in GitHub secrets.
        • 2026-09-15. The smoke's probe is the check; the judge's verdict is recorded, not enforced.
        13a.1 - Client release versions for people and bots. Future design; implementation parked 2026-09-28.
        Status

        keep the immutable versioning design below as future reference; do not use its coordinator as the client release path. The current cross-platform operator procedure is the Vercel CLI runbook at docs/crm-factory/runbooks/cutover.md. A return to the integrated workflow requires a separate decision after the CLI procedure is documented and proven.

        Future version contract

        a client version means production, not the number of times a preview was attempted. Under the proposed coordinator, the authoritative production record would be a published GitHub Release for that exact client pack. The highest Git tag, newest commit, latest Vercel deployment, or most recent successful preview is not production authority.

        New client release labels use:

        client/<packId>/v<major>.<minor>.<patch>              production
        client/<packId>/v<major>.<minor>.<patch>-preview.<n>  preview candidate
        

        The word is preview, not rc, because the label must be understandable to a client, operator, and bot without translating release jargon. Every normal ship increments the minor number and resets patch to zero. Production v1.37.0 makes the next line v1.38.0; a production bugfix is also the next minor, not v1.37.1. There is no patch/hotfix lane yet, and the coordinator refuses one until a later approved policy defines it. The first code-bearing candidate is v1.38.0-preview.1. A changed candidate becomes preview.2. Rerunning the identical commit reuses its existing preview tag; GitHub's run ID and run-attempt number distinguish the attempts. A retry therefore never invents another product version.

        A first install has no production baseline by definition

        When an exact allowlisted pack has zero published stable Releases, the coordinator starts at v1.0.0-preview.1; promotion creates v1.0.0. Cymatica and Myth Ethos are current zero-Release examples. An empty, successfully enumerated Release set is valid. Inaccessible, partial, malformed, or ambiguous Release history fails closed and cannot masquerade as a first install.

        Tags are immutable. A bot never moves, deletes, or force-pushes a release tag. Preview allocation is serialized per pack and still uses an atomic push. If two allocators collide, the loser refetches and recalculates once, then fails for review rather than overwriting history.

        Promotion is separate from preview

        An authorized bot may calculate, create or reuse a preview tag and dispatch the preview lane. Promotion verifies the exact candidate commit is reachable from the remote default branch, green runtime and smoke receipts, and an unchanged production baseline. Only then may a human-approved promotion create the stable tag and publish the stable GitHub Release. A -preview.<n> tag or GitHub prerelease is mechanically refused by the production lane.

        GitHub does not emit another workflow run for most events created with a workflow's own GITHUB_TOKEN. After the coordinator publishes the stable Release, it therefore calls the same reusable production deploy executor directly and proves the Release exists first. A stable Release published by a person still uses the ordinary Release event. Both paths wait on the same client-environment reviewer; neither deploys from a tag alone.

        Bot preview is not unattended deployment

        Tag calculation and immutable preview-tag creation may run automatically. The actual preview deploy keeps today's client-<packId> GitHub Environment and waits for its required human reviewer before Vercel secrets are available or any preview is shipped. This policy does not create a separate preview environment and does not silently remove Alex's approval. A bot must report waiting for preview approval, not deployed, while that gate is pending.

        Legacy tags remain exact evidence and receive a display normalization only: client/<packId>/v<n> is shown as v1.<n>.0. Rising Origin's last stable published Release remains exact tag client/rising-origin/v37, displayed as v1.37.0; the latest manually CLI-promoted Vercel production deployment uses source 788cd1bd and has no matching stable GitHub Release. Do not treat the tag as proof of that deployment's source or as a current deployment receipt. Unpublished tags including v35, v36, and v38 through v43 are examples of the general rule: a tag without a published stable Release is not production. They do not advance the baseline or occupy an ordinal on the new preview line. They stay in Git history and are never moved or deleted. The first new candidate line is client/rising-origin/v1.38.0-preview.1.

        Build bots use one release coordinator rather than implementing these rules independently. The coordinator reads published Releases, existing tags and the requested immutable source commit; emits an exact and normalized version receipt; creates or reuses the preview tag; and calls the existing deployment executor. Production promotion remains behind the existing client-specific human approval boundary. The complete allocator, concurrency, failure and verification contract is docs/superpowers/specs/2026-09-27-crm-factory-client-release-versioning-design.md.

        Parked coordinator implementation reference

        .github/workflows/client-release.yml retains the planned integrated allocator. It is not the current operator entry point and must not be run while parked. Its preview operation accepts packId, immutable sourceRef and dryRun; the caller never chooses a version string. A dry run enumerates and validates complete GitHub Release/tag evidence and uploads an allocation record without creating a tag or deployment. A real preview creates or reuses the immutable candidate, calls .github/workflows/client-deploy.yml in preview mode, and cannot produce artifact client-release-preview-<runId> until the human-gated deploy and enforced smoke are green. The receipt binds the exact pack, preview tag, source SHA, production baseline, run ID, deployment URL, host/pack checks and migration result.

        The promote operation accepts only packId, the exact previewTag and its previewRunId. It downloads that run's receipt, validates it through scripts/client-release/cli.mjs, proves the candidate is reachable from current origin/main, and repeats the baseline/tag validation after the human approval wait. It then creates the stable tag at that exact SHA, publishes and re-reads the stable GitHub Release, and calls the same production executor, which independently repeats Release/tag/SHA validation and retains the client environment gate. A failed production job is rerun at the same stable Release after an environmental correction; a code change starts a new preview and the next minor line.

        Client release versions are separate from the CRM Factory/platform version, database migration versions and pack schema versions. Release receipts record the factory source commit, but this lane does not change package.json or claim a new platform release.

        Current operating standard — 2026-09-28

        Client releases use the cross-platform Vercel CLI before any integrated versioning system is adopted. The coordinator and immutable version contract remain documented future infrastructure, but client-release.yml is parked and is not the active operator entry point. Re-enabling it requires a separate review and explicit approval; resolving one issue does not turn it back on automatically. Rising Origin run 36354165525 allocated client/rising-origin/v1.38.0-preview.3; the deployed preview host resolved to the unbound tenant and its root smoke returned 404. No green preview receipt or promotable candidate exists. Do not rerun or promote that run. Rising Origin then proved the cross-platform CLI standard recorded in docs/crm-factory/runbooks/cutover.md: exact merged commit 788cd1bd, verified existing project, Git-free and client-pruned source archive, Vercel remote source build as a preview first, and preview root, binding, pack, unbound-host and migration smokes. A Windows-local prebuilt deployment was proved unsafe on 2026-09-27 when deployment dpl_GY4frTE6xgTKW1zQZbNb2WXZpbYK omitted a traced Next.js server chunk, returned 500 and was rolled back immediately to dpl_VFj7KGgkb2958LEpp5vmX8wVRe7h; it must not be repeated. Production promotion remains a separate decision on the exact verified preview. That manual release creates no stable GitHub Release or version baseline. The CLI procedure in docs/crm-factory/runbooks/cutover.md is the standard path until the integrated coordinator is separately approved.

        The first Rising Origin CLI-standard run completed on 2026-09-27

        Cloud-built preview dpl_4QuWN3RZYPo8NCj2qWAQoqF4pqrW and promoted production deployment dpl_57Ku8gQK1hGLaANbwYc4vje2ctUi passed root, trusted rising-origin pack, unbound-host and migration 20260928143000 checks. This records manual activation of exact source 788cd1bd; it does not create a stable GitHub Release or advance the release baseline. The Project Request form is now an active instance-owned form under Publication 2, and its ChatbotBuilder adapter is connected. Its public-site origin allowlist is empty; the public site still uses GoHighLevel. The synthetic app Test and unverified provider-receipt status are recorded in §4b; no public cutover follows from that Test.

        The current bot-facing CRM Factory instructions must use the cross-platform CLI procedure and must not invent a GitHub version string. If the integrated coordinator is later approved, the Phase 14 factory skill must inherit its minor-only version rule verbatim. This release policy does not start Phase 14.

        The operating surface is owed, not implied

        The canonical rule lives here first, beside the workflow it governs. The Ventari Brain must receive a concise release-policy projection, and the Ventari app must eventually show Production version, Preview candidate, source commit, run receipt and promotion state from the same machine-readable release record. Until that reader and projection are built and verified, this repository and GitHub Releases remain authoritative; no manually maintained Brain page or UI label may override them.

        The cost that ships with every instance, and why it moved up

        vercel.json carries 51 cron entries, five of them every minute, and they deploy with the app. Nick's Vercel account is on Hobby (seen 2026-09-12): roughly two crons, daily only, and non-commercial terms. So the action generates the build's vercel.json from a per-pack cron allowlist in deploy/clients.json (Rising Origin: none, for the demo) and never modifies the committed root file. The demo runs on Hobby as a preview with zero crons. Production needs Pro, on his account and his bill, and that line goes in the message to him alongside the password ask. Which crons a client pack actually needs is the per-module mapping still to do; an empty list is honest for a demo and wrong for production.

        The account, not a team

        His Vercel is a shared account he created for Ventari (ventari@ at his domain), Hobby, no team members. The deploy token is minted under that account; the org id is that account's; the project is his existing rising-crm, which has no Git integration today (it deploys by CLI), so there is no old integration to disconnect at cutover.

        13b - The demo path for Nick, in order

        1. B and F merge; six surfaces converted.
        2. The gate: his real export on a local stack, host bound to his pack, Alex and the operator side by side with his current build, pack adjusted.
        3. Revised 2026-09-12: no demo database. The demo is his export on a local stack on Alex's desktop (docs/crm-factory/runbooks/cutover.md, sitting 4: nine minutes from a clean stack), recorded as a Loom. Costs nothing, needs nothing from him, and the first deployed instance he touches is the real one. Rollback is the pre-cutover snapshot plus Vercel's promote-previous.
        4. The GHL mirror and the public intake route (Phase 13d, scoped the same day) so leads reach the new instance from day one without hands.
        5. The migration: tag client/rising-origin/v1, production deploy into the same project (his link unchanged), env pointed at his real database after the live cutover. The cutover runs through the Supabase Management API with a collaborator token if that path proves out read-only first; otherwise his database password. At initial cutover his CRM shipped zero crons (no vercel.json in rising-crm). Current pack deployments ship three daily routes: the standard GSC snapshot at 06:20 UTC, the standard measurement snapshot at 07:20 UTC, and Rising Origin's automation-rules tick at 08:40 UTC. The route allowlist and override are in deploy/clients.json.

        13d - A client's GHL as a connection (landed)

        A client whose front door is still GoHighLevel connects it themselves on /settings/connections. The pack declares integrations.ghl.locationId as a guardrail only (Rising Origin: his sub-account id; agency and Helix: null). The client path stores a Private Integration token encrypted on instance_connections kind ghl; the platform path uses GHL_AGENCY_API_KEY when the pack already names a location. Connect verifies the location endpoint. The Calendars page lists that location's calendars read-only. An inbound mirror upserts people, organisations, opportunities and appointments from contacts and events, keyed on the GHL id, and feeds the attribution ledger. Nothing outbound. The agency pack has no card and no route; Ventari's retired-GHL rule is unchanged. No GHL credential enters a worker lane.

        Corrected 2026-09-15, first live catch-up on Rising Origin

        GHL's calendar-events endpoint takes epoch milliseconds and one of calendarId / userId / groupId; the mirror sent ISO bounds and no calendar and got 422. Events are now read per calendar of the location (the connection's calendarIds, else the location's list) and merged on id; the stub enforces the same contract so the verifier fails the old shape.

        Corrected 2026-09-15, after Alex walked the live instance

        Catch-up created 14 people, 0 organisations, 0 opportunities, 5 appointments. People with no first/last name and no email were stored under the GHL contact id, which reads as a code, status: 'provisional'. Live on v23: name, email, and phone are person fields on /contacts, with or without a company. Click any row. Company is optional; attaching one still opens /crm/clients. Soft-archive of a company still lives in the org workspace (9g-2). Conversations are not in the mirror. lib/ghl/client.ts is GET-only and has no conversations client; Nick's GHL SMS/email/WhatsApp stay in GHL until a later lane, and that lane needs a conversations scope on his Private Integration token (today: contacts, opportunities, calendars, calendar events, locations). Messages → Social DMs is X/Instagram/Facebook, not GHL chat.

        13e - GHL exit, three replacements (scoped 2026-09-15)

        Not a big-bang. Nick works in one CRM; GHL is cancelled when the third replacement is live. Detail and file paths: docs/crm-factory/ghl-exit.md.

        Native intake factory capability, merged 2026-09-26; Rising Origin code deployed 2026-09-27, public cutover held

        The shared build installs this capability inertly in every CRM: installation is not configuration, configuration is not publication, and publication is not verification. Admin actions separately create a public URL, publish a form and configure an adapter. The client-specific live state is recorded in §4b. Shared migrations and templates seed no client form copy, allowed origin, credential, provider locator, pipeline mapping or publication.

        • forms remains the single inventory with explicit agency_catalog and instance ownership. Catalog refresh can update only catalog-owned rows. Instance drafts are database-owned; each publish creates an immutable form_publications snapshot and moves the current-publication pointer.
        • The neutral project-request-v1 template creates an inactive instance draft. A Project Request can match the same person as a Journey Access Checklist, but it cannot create, verify, waive, complete or advance a checklist item.
        • form_adapter_connections is a service-only multi-row store, so several forms and several provider credentials coexist without changing the one-row-per-kind semantics of instance_connections. Secrets use the instance encryption key and appear only once on create or rotate; status payloads expose safe state, counts and timestamps, never ciphertext.
        • Native form, legacy website and ChatbotBuilder ingress all normalize to one bounded envelope and call the same atomic service. A successful commit writes the person/organisation/opportunity mutation, durable idempotency receipt and immutable intake evidence together before a retryable post-commit automation dispatch. Evidence explicitly records executionAuthority: false; conflicts are preserved as evidence and do not overwrite canonical facts. Staff read the bounded evidence through /api/clients/[organizationId]/brain/evidence/intake, with exact instance identity binding and no-store responses.
        • ChatbotBuilder uses one connection per form/provider row. Bearer Authorization is the shipped mode. The provider-editor probe could not be completed, so no JSON-body secret fallback ships. The approved architecture permits that fallback only after a recorded failed header probe, only in preview, with the secret removed before validation, digesting, evidence or logging; production would still need a separate explicit exception.
        • Provider ownership stays outside the CRM. The client owns the ChatbotBuilder and Resend accounts. Direct ChatbotBuilder SMTP uses smtp.resend.com, port 465 SSL, username resend, and a client-owned sending-only domain-restricted key entered in ChatbotBuilder. Opens and clicks for provider-sent mail are outside current CRM receipts; CRM-sent mail retains its existing Resend receipt path.

        At merge, the capability's build proof was local and synthetic: schema, real disposable-Postgres runtime, renderer, public routes, adapter lifecycle, canonical ingress, Forms, Connections and factory-presence checks were green. That evidence did not itself authorize hosted migration, a live form, website embed, production traffic switch or deployment. Rising Origin's later migration and deployment are recorded in §4b. They still do not prove a real website submission or provider request. Packet A remains parked, and each public cutover retains a separate approval gate. Phase 14 stays blocked.

        1. Form intake (first, pure client lane). Built 2026-09-20; not yet released or live-proved
          His website POSTs a Project Request to GHL form yOljZNFvrgWLPtSfHlwD today. The replacement: POST /api/intake/[packId]/[formKey] on the instance host, pack-driven (pack.integrations.intake: enabled, formSource, allowedOrigins; Rising Origin declares it with his two site origins, the agency declares it with ventari.media for the proof; beeem and Helix do not). A Website form card on Connections: one Connect button that mints the form key and shows the address; Disconnect turns it off; connecting again mints a new address. The card shows nothing else - no secret, no rotate. (The API also mints an HMAC signing secret, returned once to the admin call, for a client whose site posts server-side; the card does not surface it.) One submission becomes a person (matched by email), an organisation when a business name was given, and a deal at the first stage of the pack's pipeline with attribution evidence (formSource, landing URL, referrer host, UTM) - the GHL mirror's write pattern with source intake; the same person inside ten minutes is one deal. Fields are the canonical keys (fullName, email, phone, businessName, website, businessType, need, problem, timeline, budget, consent) with his GHL form's labels accepted as aliases; extras kept, bounded. Deviation from the 09-15 wording, recorded: "signed with a per-connection secret" assumed a sender that can keep a secret. His site is static (Vite, no server), so a secret in the page would be public - and so is the form key, which sits in his site's code: it is an address, not a secret. What stands between the endpoint and junk, stated honestly: the pack's origin allowlist (CORS - a browser cannot fake it, curl can), persisted rate limits that fail closed (12 per minute per IP, 60 per hour per endpoint so a spread of IPs cannot flood the pipeline), a honeypot field, and the duplicate window. The same posture as Formspree or Netlify Forms. The consequence of abuse is junk deals the client can delete. Named upgrade, not built: if junk ever becomes a nuisance, a Cloudflare Turnstile token check - intake.turnstileSiteKey on the pack, the Turnstile secret pasted on the card, one verify call in the route, one script tag on the site; half a day, no change to anything above; the route behaves as today when the pack has no key. Decided 2026-09-20 (Alex): not now - nothing is built until one real junk submission lands. When it does, Turnstile over reCAPTCHA: invisible for nearly every visitor where reCAPTCHA v2 shows a puzzle that costs leads; a pass/fail answer where v3 returns a score to tune; no badge required; no Google tracking on the client's site; free. It needs a Cloudflare account (widget = site key + secret), not a Cloudflare-hosted site; the CRM's only contact is one server-side verify call per submission, failing open if Cloudflare is down so a lead is never lost to a spam filter. Whose account: the client's (the Meta and Resend rule). Rising Origin already runs risingorigin.com DNS on Cloudflare, so his widget goes in his account; Ventari's Cloudflare account only for a client who has none. His DNS being on Cloudflare protects his site, not the CRM endpoint on Vercel - Turnstile is still the form's guard. No-account fallback if a client refuses Cloudflare: a proof-of-work token computed in the page, same shape, stops scripted spam, not a determined sender. Vercel BotID does not fit (it guards routes in the page's own Vercel project; the site and the CRM are separate projects). The HMAC signature (x-intake-timestamp, x-intake-signature, five-minute window) exists and is enforced whenever a request carries it, and required when the connection says so - the one guard curl cannot pass, for a client whose site posts server-side. verify:intake (core-stability) covers the contract, connect/disconnect with the secret encrypted and returned once, the absolute address, field normalisation, the write path on an in-memory CRM, the duplicate window, the signature, and the card (Disconnect only when connected; no secret rendered). Proof order (Alex, 2026-09-20): the agency instance first - fan out 20260923120000 to Ventari's database, Connect the card on the agency instance, one post with an allowed Origin becomes a deal at the first stage of ventari_sales, the same post again is a duplicate, a post from a foreign origin is 403; then v30 for Rising Origin. Exit: his /contact form posts to the endpoint and a submission is a deal on rising-crm within seconds; then the GHL form is retired. The site-side change is in his site repository and needs the explicit ask §13e names.
        2. Automations (product then client). Built 2026-09-21 (13e.2); not yet released or live-proved
          The two GHL form workflows ("email the owner; email the requester") are two rule templates on the client's own pipeline (lib/automation-rules/client-intake-rules.ts): trigger stage_entered at the pipeline's first stage, condition source in [intake], action send_email - new in the rule schema, to: team | contact, subject and body with {{name}} / {{deal}} / {{stage}}. On a client instance the action sends through createResendSender(), which resolves the client's own Resend key from Connections and refuses without one; the team address is only what the client connected (mailbox, else the sending from-address, else the pack's declared inbox) - never an environment fallback. On the agency pack send_email never sends: it degrades to the review draft, so founder decision #1 (no automated outbound from the agency) and the runner verifier's literal assertion on actions.ts stand unchanged. The intake route fires stage_entered inline after a created deal (runRulesForEvent, fail-open), so the stage-entered rules need no cron. Follow-ups that fire a number of days after a deal is created (days_after_created) do need the daily rule tick, /api/cron/automation-rules. The client pack's entry in deploy/clients.json carries it once a day (40 8 * * *); see Standard client crons. The ventari tenant literal was never the problem: every instance's database uses it (packs carry tenantLiteral: 'ventari'). Seeded per instance by scripts/intake/seed-client-intake-rules.ts --pack <id> [--apply] [--activate]: refuses the agency pack, a pack without intake, and a mismatched instance_identity; gates land is_active=false; --activate refuses until the client's sending key is on Connections. Rising Origin's pack now exposes three Automations pages (General, Rules, Runs; the rest answer 404 for it, pack-driven in lib/automations/surface-access.ts), so once that build is deployed its rules are switched on from General with the client; until then, and for any pack that does not declare the surface, that flag is how a client's rule is switched on. verify:client- automations (core-stability) covers schema, templates, evaluation on a client subject, the four send outcomes, the agency degrade, the event wiring, the boundary and the seed's refusals. Exit: on his instance, one real website submission emails info@risingorigin.com and the requester, from his own address, and both land as ran in Runs.
        3. Booking as a pack surface. Built 2026-09-21 (13e.3); not yet released or live-proved
          Decided 2026-09-19 (§5 item 12). What changed: one route loader for the instance (lib/booking/routes-for-instance.ts) replaces fourteen private copies
          • the agency keeps rows-else-its-defaults plus the Helix route; a client instance gets its booking_routes rows and nothing else (an empty table is "no routes", never Ventari's Calendly links, never Helix); an unbound host gets none. The public page, the slot picker and the unavailable page render in the pack's tokens and the unavailable page emails the pack's declared inbox. Booking copy on a client instance names the product (clientBookingBrand), never the agency or Helix. The shared busy calendar is the agency's only: a client instance never reads ventarimarketing@gmail.com; its hosts' own calendars are the only busy source. The instance is resolved from the deployment's own production host through the host bindings (lib/tenancy/instance-pack.ts, one home for the lookup the sender and the rule runner carried inline). Host = the person whose Google is connected on the client's Connections card; the seed (scripts/booking/seed-client-booking-route.ts --pack <id> --host-email <email> --origin https://<host> [--apply]) refuses a host with no calendar, the agency pack, a mismatched instance_identity, an existing key, and never writes a Calendly target. Known gap, on purpose: booking-reconcile runs every ten minutes on the agency; Vercel Hobby allows daily crons only and the client allowlist copies the source schedule, so his crons stay empty - the public status endpoint is the fast path and reconcile is crash recovery; a per-client schedule override is the fix when it matters. verify:client-booking (core-stability) covers the loader per instance, fail-closed hosts, the env-bound pack, brand copy, shared busy, the surfaces, and the seed's refusals. Exit: his site's Book-a-Call points at rising-crm.vercel.app/book/intro-call; one real booking lands on his Google calendar and as an appointment in his CRM; the GHL widget is retired; he cancels GHL.

        His site www.risingorigin.com (pack productionHost is null): Book a Call widget https://api.leadconnectorhq.com/widget/booking/9r0PXwwRF6yzBe9MJSwj on / and /fractional-cto; Project Request form https://api.leadconnectorhq.com/widget/form/yOljZNFvrgWLPtSfHlwD on /contact. Chat widget off (Alex, 09-15). Site cutover still needs an explicit ask.

        Forms tab pack-aware visibility, fixed locally 2026-09-28; not yet released.

        Finding

        A client instance's Forms tab seeded and listed Ventari's four agency-catalog forms — Free Brand Audit, whose copy named Ventari, and three Ventari onboarding drafts — because reading the tab seeded the agency catalog into whatever database served the request and listed every row under the shared ventari tenant literal. Instance forms also showed Draft — not built yet and 0 fields, because the card read catalog metadata instead of the instance's own publication state. Observed on Rising Origin's Forms tab, on the Project Request form at Publication 2. This lane made no hosted read of Rising Origin's database: it does not know or claim how many leaked catalog rows sit there today, and none were removed.

        Decision

        The agency catalog is the agency instance's own surface. A client instance neither seeds nor lists it. An unbound or unknown host is treated as a client, not the agency, so the default fails closed toward hiding Ventari material. Catalog rows already leaked into a client's database are hidden, not deleted. Free Brand Audit's public route, intake contract and workflow are unchanged. Instance forms now show their real publication state and field count instead of catalog placeholder text. A reusable template built from Free Brand Audit would be a separate, explicitly installed client-branded template — this fix does not create one.

        The route decides agency versus client by resolving the pack from the request's own host, never from a stored preference: GET/PATCH in app/api/automations/forms/route.ts call isAgencyFormsHost (new, lib/forms/visibility.ts), which calls the same resolveTenantContext the publications route already used and reads agency only when the resolved pack is the Ventari agency pack. Any other result — including an unbound or unrecognised host — resolves to client, so a host nobody has registered shows nothing from the catalog rather than everything.

        Test coverage. scripts/verify-automations-forms.ts proves the chain in five layers; the fourth layer also has a matching console-level render check:

        • Store behaviour, given an isAgency boolean (commit b99ddfdc): the agency case seeds and lists exactly the four FORM_CATALOG rows plus instance rows; a client case (generic in the fixtures, not the named client, to satisfy verify:no-client-material) never seeds, hides a pre-existing leaked catalog row, and returns only its own instance row; an unbound/unknown host seeds and lists none of the catalog; a client PATCH on a catalog row is refused with the same not-found response a missing form gets, and the row is proved untouched.
        • The host decision alone, section 9 (commit 804d5673): isAgencyFormsHost, called against the real resolveTenantContext (no stub), resolves the named Ventari agency host and the local-dev fallback to agency, and a synthetic client host, an unbound host and a null host to client. Sections 9 and 10 together are the durable coverage of host → resolved pack → visible forms; nothing else in this lane proves that chain.
        • Host through to what the tab actually shows, section 10 (commit 5bd8c735): the same real host resolution is chained into seeding and listing for a client host, an unbound host and the agency host, proving no catalog row is seeded or listed for the first two and the instance row is what a client sees either way. A structural check reads the route file itself and confirms it calls isAgencyFormsHost exactly once in GET and once in PATCH, passes { isAgency } into every store call, and imports nothing else pack- or tenant-shaped.
        • Instance card status and field count, proved from the real publication, section 11 (commit 367c44ea): the real listForms/toDto are called (no hand-built DTO) against PROJECT_REQUEST_TEMPLATE.definition's actual 12 fields. A published instance (draft 12 fields, publication 5 fields) renders status: 'live' and fields.length === 5, keyed to the publication, not the draft; the same row, after its draft is rewritten to 8 fields through the real updateInstanceFormDraft, still shows the publication's 5 — the published snapshot does not move when the draft changes later. An unpublished instance shows its draft's own field count. A Forms card render of that same real DTO (scripts/intake/verify-native-forms-console.tsx) confirms the card itself prints "Published" and "5 fields" from that real data, not from an invented fixture.
        • Fail-closed completeness boundary, section 12 (commit edf603c1): GET /api/automations/forms/<slug>/completeness now resolves the same host decision first, before requireCrmStaff() and before any database query, and returns the identical not-found response for a client host, an unbound host, and an unknown slug on the agency host — proved with a database stand-in that throws on any query, so a passing assertion means no query was attempted. A chained real-agency-host case shows the moved completeness calculation still matches the unchanged original function on the same fixture data (agency behaviour unchanged), and a structural check confirms no db.from( remains in the route and both rejection paths call the same responder.
        Review evidence, not an automated test

        On 2026-09-28 the operator ran three real mutations against the working tree, each restored exactly afterward; the working tree was clean after all three: forcing isAgencyFormsHost to return agency for every host (M1) failed the verifier at the client-host assertion; making instance fields read from catalog metadata again, as before this fix (M2), failed at the published field-count assertion; removing the completeness fail-closed guard (M3) failed at the no-database-before-checks assertion. After all three were restored, verify:automations-forms passed 37 checks and the working tree was clean. This is what shows the tests exercise the real decision, not an in-test probe — an earlier version of this fix included a hard-coded "always agency" probe inside the test file itself and credited it with that proof; cold review (reports/03-cold-review.md, finding 2) found the probe could not fail against any real bug, so it was removed (commit 367c44ea) and this operator evidence replaces it.

        The full verification set — verify:automations-forms (37 checks), verify:native-forms-console, verify:form-publications, verify:native-intake-golden-path, verify:client-surface-leak, verify:no-client-material, and typecheck — passed after each commit, per reports/01-implement-pack-aware-forms.md sections 01, 01b, 01c, 01d and 01e.

        Architecture assumption and follow-up

        The Forms store, instance form publication (lib/forms/publications.ts) and the completeness queries all use the shared tenant_id = 'ventari' literal. That is correct only while each client instance has its own database — the factory's stated one-database-per-client model. This lane did not assess that model further and did not change tenancy. If any database is ever shared between clients, the shared literal becomes a defect and needs tenant separation. Follow-up: confirm the one-database-per-client assumption for each instance at install time. No tenancy refactor was made here.

        Remaining rollout steps

        This fix is local and unreleased. Reaching Rising Origin needs, in order: merge, a client release through the current CLI standard (§13a), then a logged-in walk of Rising Origin's Forms tab (only Project Request should be visible, with its correct published state and field count) and of the agency's own Forms tab (unchanged, still the four catalog forms plus any instance forms). Whether to archive the catalog rows already leaked into client databases is a separate, optional decision, not part of this fix. No release, deployment or hosted change was made by this lane.

        The first real install, 2026-09-13

        Rising Origin is on the new CRM at his existing URL, on his existing database, with his logins. Cutover through the Supabase Management API with a collaborator token (no password), twenty minutes. Release client/rising-origin/v5 through the deploy action, after four releases each caught one first-run gap (environment, heap, upload quota, env-file tracing) and a fifth found the first Phase 13c prune (a Ventari cron above the Hobby timeout cap). Full account in docs/crm-factory/runbooks/cutover.md, sitting 5. The rule it produces: the deploy action is dry-run against the client's project the day before the window, never during it. Alex's first words on the funnel: the deals are there, the UI needs passes. That is the gate evening, now on the live instance.

        Exit: a second client is installed end to end using the runbook, by someone who did not write it, without asking a question.

        That second install is the acceptance test. One successful install proves nothing except that the person who built it can use it.

        Phase 14 - The crm-builder skill

        Full specification textdetail for agents and careful review

        The skill is the runbook, once the runbook has been executed twice by people and has stopped changing.

        • Lives as a skill in the agents tab, discoverable by any agent doing client work, bound through the pack's skills contract (SkillBinding { id, enabled, requiresCapabilities }).
        • It encodes the sequence, the check at each step, and the stop conditions.
        • It refuses rather than guesses. A missing brand asset, an unregistered pack, a database that fails its post-install check: each is a stop with a message, never a default.

        Exit: an agent runs the skill and produces a client CRM that passes the same checks a hand-built one does, including verify:client-surface-leak.

        Preconditions, all mandatory. Do not start this phase without them:

        1. Phase 13's second install succeeded, run by someone who did not write it.
        2. The sequence did not change during that install.
        3. Every step has a check that can fail. A step nobody can verify cannot be in a skill, because an agent cannot tell whether it worked.
        4. A human has opened the resulting instance and walked it. Section 5b is the argument: the first walkthrough of a client instance found four problems in minutes that 72 automated checks could not see, because the checks asserted on resolved values and the leaks were in what the page emitted. A skill that installs a CRM nobody has looked at will install the same invisible defects every time, faster.
        5. Phase 9c's token contract is written and at least the shell, funnel and CRM lanes have converted. The skill's design step is: run the extraction script against the client's current CSS, render, put it beside their current build, and stop for a person. A skill that skips the gate ships Ventari with the client's logo on it, which is what the instance looked like on 2026-09-11 before anyone said so.
        Readiness checkpoint, 2026-09-24

        The architecture, first canonical client, pack-aware migration path, pinned release path and human walkthrough are proven. Phase 14 remains blocked by the second canonical shared-core install, proof that the ordered sequence no longer changes, and one inspectable run receipt that names every check and approval. Beeem's standalone application is useful delivery evidence, but it does not satisfy that acceptance test. The dated pickup is checkpoints/2026-09-24-self-learning-crm-factory-orientation.md. Corrected 2026-09-29: the client release path proven since that checkpoint is the cross-platform CLI standard (§13a); the GitHub release coordinator is parked (§13a.1). The current gate list is §4b Phase 14 gates.

        Learning contract

        The future skill may propose a durable lesson only from a named run with a verified result, sanitized evidence and an explicit human accept/revise/reject decision. Accepted lessons update a canonical spec, runbook, verifier or skill body through review. A lesson never authorizes a migration, release, provider change, deployment or production enablement. The skill records stop conditions and refuses ambiguity; it does not learn around them.

        Client CRM Factory · Build standard

        Scope boundaries

        Stage editing

        Nobody can rename, add or reorder a pipeline stage today. Every write to pipeline_stage_definitions lives in a verify script, and there is no UI or API. This is a Ventari application improvement rather than an install problem, and it is parked deliberately. It is named here because "stand up a CRM and customise it from there" is not true until it exists, and a client CRM nobody can edit is a bespoke build rather than a product.

        Paywalled tabs

        Floated on the 2026-09-09 call and explicitly not worked through at the time. The pack contract now has modules, entitlements and surface requirements, so a client build can fail closed when a required entitlement is off. What remains outside this specification is the commercial activation path: purchase, plan change, entitlement grant and client-safe catalog data. A client Shop must never inherit Ventari's price book.

        A3 and the isolation rollout. Demoted on 2026-09-10: peer clients never share a database, so it is hygiene for Ventari's own instance rather than a client blocker.

        Client CRM Factory · Current operations

        Program status, updated 2026-09-28

        Full specification textdetail for agents and careful review

        CRM Factory Roadmap — Eight Items

        Updated: 2026-09-28

        This is the approved roadmap wording for the CRM Factory spec's §4b summary. It records status and sequence; it grants no execution authority.

        Message Triage and native flows, 2026-10-07

        These are not one of the eight intake items. Current status and the ordered reuse plan are the Phase 9i correction. Human social triage and native flows stay separate. social_dm_triage is on for Rising Origin and Ventari Agency. Native flows remain off. No client migration, deployment, activation, or live provider test follows from that note.

        Current next action

        Obtain Edgar's ChatbotBuilder workspace access and confirm who can securely provide the authorized credential; separately confirm the intended Rising Origin sending account with its account owner. Do not put secrets in chat. Before any real provider test, verify the CRM tenant/database binding. Any test that writes to the production CRM database requires separate explicit approval.

        Two separate finish lines

        • Rising Origin intake: the provider, email, and public-form paths each have their own evidence; booking remains a separate cutover.
        • CRM Factory Phase 14: the second canonical install and its inspectable run receipt are complete.

        The roadmap is informational. Status labels do not authorize credential handling, production writes, public routing changes, migrations, client releases, or deployments.

        Phase 14 gates (checked 2026-09-29 against main 11a0d8c4)

        Phase 14 starts only when every mandatory precondition in its phase body passes. This table is the single current record of each one; the phase body holds the requirement, this table holds the evidence.

        This table records evidence and grants no execution authority; every ledger step still waits for the person named in its Gate column.

        Next Phase 14 step

        complete one second canonical install, run by an operator who did not write the sequence. It may be a real client or an isolated synthetic installation under a fictional name such as "Sable Ridge Coaching (synthetic)"; no real customer or real customer data is required. Either counts only when all of these hold:

        • it uses the shared Ventari codebase and runbooks/install-sequence.md;
        • the operator is not the sequence's author;
        • every step runs in ledger order, and the order does not change;
        • one closed receipt records passing evidence for every step and passes verify:crm-factory-receipt;
        • a named person walks the instance on desktop and at 390 px.

        A synthetic installation also needs two things:

        • its own Supabase and Vercel projects, dedicated to it and separate from Ventari's own instance, standing in for the client-owned projects of ledger steps 3 and 4;
        • a named person other than the operator who acts as the client and holds every client gate, so the receipt's approvals stay separate from the operator.

        Its pack is written and registered like any other (step 5).

        A prototype, a design mock or a tabletop walk-through of the sequence does not count. Until the install passes, the sequence stays status: candidate and inactive, and none of its Activation edits land.

        After Phase 14 — candidate roadmap

        Phase 14 remains the immediate CRM Factory acceptance goal. The candidates below are for prioritization after its acceptance gates pass; this roadmap does not change those gates or authorize implementation, client use, release, or deploy.

        These are roadmap candidates, not current capabilities or work in progress. Phase 14's install sequence and acceptance evidence remain the immediate finish line.

        For the team, the phase table below: one line per phase; the detail is in each phase; the rules at the end bind everyone who merges to main. Dated corrections between here and the phase table are facts from the live instance, not a new plan.

        Verified snapshot on 2026-09-24 (superseded by the release correction below)

        Rising Origin production was v37 at 91eb3cae. It includes the shared analytics bridge and optional GA4 and Meta measurement work from PR #966, the SEO mobile layout from PR #972, the client-deploy author fix from PR #973, and the X OAuth exchange/status repair from PR #979. The X source fix and client deploy are proved; a fresh authorized reconnect still has to prove that the saved connection persists in production. PRs #969, #974 and #975 are closed without merge and no longer hold ownership. Their useful findings are reconciled here against current source. Any surface implementation starts with a new overlap check and preflight from current origin/main.

        Release record correction, 2026-09-28

        The last published stable GitHub Release remains client/rising-origin/v37 (source 91eb3cae). The client-release.yml attempt allocated preview v1.38.0-preview.3, but its post-deploy root probe returned 404 and it was not promoted. The later cross-platform CLI run used exact source 788cd1bd; its Vercel production deployment dpl_57Ku8gQK1hGLaANbwYc4vje2ctUi passed root, trusted-pack, unbound-host and migration 20260928143000 checks. That manual production deployment did not publish a stable GitHub Release or advance the stable version baseline. Keep the running deployment and stable release record separate; the CLI runbook is current and the integrated coordinator is parked.

        Rising Origin native-intake audit, read-only, 2026-09-28

        PR #1021 is merged, and the shared native intake capability is in the manually activated production source 788cd1bd; the native-intake migration was applied to both Ventari and Rising Origin databases. Rising Origin has one active, instance-owned Project Request form. Publication 2 is current, but its allowed_origins is empty, so this publication is not ready to embed from the public site. There is a second, code-level boundary: the current submit route compares the browser Origin only with the CRM API's own origin and does not read the publication's allowed_origins. Therefore a website on risingorigin.com would still be refused even after the origins are added to a publication. Before any cross-origin website embed, either make the submit route enforce the published origin allowlist or use a reviewed same-origin server-side proxy; test that exact contract. The public Project Request and booking paths remain on GoHighLevel. The ChatbotBuilder adapter row is connected, bearer-header mode, environment=preview; Alex confirms that the Forms-panel synthetic Test ran successfully. Its verification timestamp is not evidence of a real provider request: last_received_at and existing intake receipts are empty. No automation rules exist, so this intake path has no configured CRM email or task action. The 2026-09-28 baseline is 31 people, 11 organizations, 11 opportunities, 20 stage-history rows and 12 automation runs; tasks, receipts, evidence, dispatches and client-program intake rows are zero.

        Controlled native-form proof status, 2026-09-28

        Alex parked Packet A. It would create a Vercel preview but write a labeled synthetic person, organization and opportunity into Rising Origin's production database; it does not test a real ChatbotBuilder request. Packet A has not run: no temporary attempt secret was added, no Packet-A preview or submission exists, and no receipt exists. Worker 06 is a read-only before/after verifier and must wait until a proof receipt exists. If the proof is resumed, first revalidate the read-only baseline and marker, verify the host/pack binding and independently confirm the Supabase project ref is Rising Origin (aoylknoetehsmghkbchk) through a live project-info read. A pack_id = rising-origin row or hostname alone does not prove which database the service-role client targets; never print or copy the key value into a report. Confirm the preview-only NATIVE_FORM_ATTEMPT_SECRET before the attempt route is used. Run only under a new explicit approval; replay the identical submission to verify idempotency; compare exact table deltas; remove only the exact temporary preview and variable; archive synthetic business records through supported UI actions; retain immutable receipts and evidence. Website, booking and GoHighLevel routing stay untouched.

        Forms catalog tenant-boundary finding, 2026-09-27

        Rising Origin's Forms panel shows Ventari's agency-only Free Brand Audit alongside its own instance form. Source review found that the shared Forms endpoint seeds and lists Ventari's agency catalog without a pack-aware visibility filter. This is a tenant-boundary product defect, not just awkward copy. The correction is recorded in the §13e subsection "Forms tab pack-aware visibility, fixed locally 2026-09-28; not yet released" (merged to main as #1071, 38c93a6d); it has not been released to any client.

        Next CRM Factory work, in order. This is an ordered backlog, not permission to change production:

        1. Prove Edgar's actual ChatbotBuilder request when his workspace is ready
          Confirm who can provide the existing credential without exposing it in chat; do not rotate it by default. Before sending a request, verify the endpoint's actual tenant/database binding: the adapter row's environment=preview does not by itself prove database isolation. Use a labeled synthetic payload and exact replay in a controlled environment, with no active email/task rules; record receipt, idempotency, tenant identity and the absence of cross-tenant effects. Any production-database write needs its own explicit approval. Where it stands: the adapter row is marked connected in environment=preview, bearer-header mode. Alex's app-level Test in the Forms panel is synthetic: it checks bearer authentication against the stored secret and the envelope handling, confirms the digest is repeatable, and checks the handoff mapping when the payload is a handoff. It makes no provider request and creates no business records. Its only database update is connection verification metadata: it sets last_verified_at and clears last_safe_error_code. It is therefore not proof of a real ChatbotBuilder request. The 2026-09-28 snapshot showed no received timestamp and no intake receipt, and no automation rules were configured. Finish line: one real ChatbotBuilder request recorded with its own intake receipt, received timestamp, replay result and tenant identity. Until that receipt exists, this path is configured, not proven.
        2. Keep sending paths separate
          ChatbotBuilder-owned nurture through the client's Resend SMTP account is distinct from CRM-sent mail. Verify the client's sender/domain and provider test separately. Do not enable CRM email actions until the client-owned sending connection, rule, recipient and copy are configured and reviewed. For each path, verify the intended Rising Origin sender, the Rising Origin account it sends from, and its delivery path; never use Ventari credentials for either. Finish line: each path has its own delivery receipt. A receipt for one path does not prove the other, and neither path is proven without its own.
        3. Keep public cutovers separate
          Before embedding the native form, create a reviewed publication with the correct apex and www origins, and first resolve the submit route's current same-origin-only check (the publication allowlist is not currently enforced by that route). Then prove the website-to-CRM flow and rollback path. Do not route the live site away from GoHighLevel until that proof passes. Booking is a separate cutover. Packet A remains parked unless a native-form proof is needed; if resumed, it needs a fresh baseline, live database-project identity check and explicit approval because its current design writes synthetic business records to the production CRM database.
        4. Complete the reusable factory proof before Phase 14. A second canonical one-codebase client install and one inspectable run receipt must prove the ordered install sequence. Beeem's standalone app remains useful learning but does not satisfy that acceptance test.

        Beeem is now a live standalone CRM at https://beeem-crm.vercel.app, backed by its dedicated Supabase project and deployed from joyblisscoder/beeem-crm by Vercel's Git integration. The 2026-09-23/24 commits added staff access control, origin-restricted capture, Shopify customer/order/newsletter ingestion and a production deploy. It was fast and useful. It did not use the shared client/<packId>/v<n> release path, is absent from deploy/clients.json, and has its own application source. It is therefore a live client tool and a factory input, but not the second canonical one-codebase install required by Phase 14.

        Corrected 2026-09-29: that tag path is now parked (§13a.1); Beeem is excluded because its source tree is separate and it is not in deploy/clients.json (§1).

        Reporting, 2026-09-23

        Phase 13g is the website-reporting contract in this code. The client-facing standard is the existing Website analytics connection, Search Console, and separately labelled CRM outcomes. The six card states and the Pacific-day presence count are in this branch. No client release, hosted migration, or live website change has carried them. An existing client stays on the last approved release. Phase 14 stays unstarted. The table below remains the 2026-09-20 account of what is already released.

        Pickup

        Alex stopped 2026-09-15, resumed 2026-09-19, and the night of 2026-09-19/20 took Rising Origin from v24 to client/rising-origin/v29 at https://rising-crm.vercel.app (ship and smoke green, migration version asserted). Live on his instance: Meta on his own app (9f, exit = his first post), GHL re-entered after a key rotation, Google sign-in, every Connections card with its own how-to, a database that reports its version. Merged to main the same night: #831 (§5 answers), #833 (9f), #834/#835/#845 (deploy action), #836 (Instagram on graph.facebook.com), #837/#843 (findings), #839 (Connections copy), #841 (Phase 12 hardening), #844, #846. Counts: 34 core migrations reached his database in one fan-out; 3 are agency-scoped and skipped by design; Ventari's database 369 applied, 0 pending; his 356 applied, 0 pending (measured 2026-09-20 07:58 UTC). §4b is the call script. Next build: 13e form intake (scoped, decided), then 9h (scoped), then Phase 10 when beeem is an instance. Tuesday with Nick: his five connections, the Search Console property, a Supabase custom domain, v30, his first post. Do not cancel GHL, do not point his website at the CRM, do not demo Ask Executive on his instance until 9h ships, do not start the skill, do not deploy beeem.

        Read, in this repo, docs/crm-factory/:

        Fan-out is scripts/apply-migrations.mjs. Agents edit the files in this folder, not the hosted page. The hosted page is a render of origin/main.

        How far from the skill (Phase 14)

        The architecture (phases 1–8) and Nick's install (9, 12, 13a) are done. The skill's own preconditions are not: preconditions 1, 2 and 4 are not met, 3 is partial, and no run receipt exists (see §4b Phase 14 gates). No second client has been installed by someone who did not write the runbook, and the sequence is still moving (13e GHL exit, 13f skill bundling, analytics activation, module pruning). Decision 2 (where a skill body lives) was decided on 2026-09-19 (§5, item 2); its client half is 13f, approved and not yet built. The current gate list is §4b Phase 14 gates. Do not start Phase 14.

        Downtime and lessons, recorded

        his CRM was down two hours on 2026-09-13, twenty minutes of which was the cutover; the rest was the deploy path meeting five first-run gaps live. Rule: dry-run the deploy action against the client's project the day before, never during. Also: never build a Vercel prebuilt output on Windows; a collaborator token through the Management API replaces asking for a database password.

        Where it all lives

        the product and every merge in joyblisscoder/VentariFullApp. Pickup is this folder: docs/crm-factory/ (https://github.com/joyblisscoder/VentariFullApp/tree/main/docs/crm-factory). Agents edit SPEC.md here, same PR as the code. GHL-exit detail is ghl-exit.md. Cutover procedure is runbooks/cutover.md. UI rules are BUILDING-ON-THE-CRM.md. Fan-out is scripts/apply-migrations.mjs. His instance is on his Supabase and Vercel projects. The operating desk is Alex's and is not required to pick up this spec. The Ventari Brain one-page concept is owed. The intended future route is https://docs.ventari.media/crm-factory/. It is not yet published or live-verified. Once publication is approved, render from origin/main and republish through the shared Wrangler docs host. Routine regeneration does not require a new architecture review.

        BacklogDated handoffs and notes, kept for the record

        What the team should know, plainly.

        1. Every migration merged to main reaches every client instance with that client's next release, in order. The two from 2026-09-14 (client_approval_batches, the plan-approval function) are already on Rising Origin's project. Local stack, then Ventari's project, then clients. The fan-out script is scripts/apply-migrations.mjs.
        2. Nothing changes how Ventari's own instance looks or behaves without you. Every lane proves it with a pixel harness on the agency binding (fourteen surfaces, fourteen zeros). One lane this weekend failed that test and was held, not merged.
        3. The spec lives here, with the code, and changes in the same PR. The Ventari Brain one-page concept (Concepts/operations/crm-factory.md) is owed; do not wait on it. This folder is the pickup.
        4. Resolved: Ventari's production Supabase lives in the info@ventarimarketing.com Supabase account; Alex has direct access and runs the fan-out there first with a token from that account (Phase 12). On the database: nothing to do now. The Ventari Facebook Business and developer app are the agency's own connection, never a credential source for a client instance (9f).
        5. Open, for both: the before/after of Ventari's funnel and CRM from the design pass, if you want the agency to take any of the seven decisions itself. Today it takes none; the roles let it choose later.
        6. Nick's GHL is still the front door (booking widget, project-request form, his messaging). The CRM has the catch-up contacts; it does not have his GHL threads. Messages → Social DMs is X/IG/FB, not GHL, and must never name Ventari's handle on his instance. Next ship we want is website form → his CRM (13e). Booking and auto-emails are product calls. GHL message mirror after that, with a conversations scope on his token.
        7. Contacts that look like codes are GHL people with no name stored as the GHL id, provisional. Click the row: name, email, and phone save on the person even if they still have no company. Company is optional; Open in CRM appears once they have one. 7b. Settings → Session must not say Ventari on a client pack. Copy reads useTenantChrome().productName (AccountSettings.tsx). Agency still reads "Ventari"; his instance reads his product name.
        8. Ask Executive is a Ventari seat, not a client feature, today. The floating button is not mounted on his instance. Ventari cannot pay for his tokens. If you want it in the product, say so; the next build is a Connections card for his model key, using the gate that already exists. Do not demo it on the Nick call.
        If you pick up while Alex is gone

        Whoever holds the code: edit this spec in the same PR as the code; 9f's code is built and its live client proof is next; do not skip the pixel harness on agency chrome; do not start Phase 14. Whoever owns the product call: yes or no on form intake (13e), native booking for a CRM buyer, auto-email on new deal, Ask Executive on the client's own key. Audits & Proposals is Nick's call, not a build.

        The Nick call — what to do, in order.

        Alex may be offline. Run it from this list. Log in at https://rising-crm.vercel.app (his CRM). Do not use my.ventari.media.

        1. Connections (/settings/connections). Confirm GHL still says connected (location D69DwDSb42TQZSwXsRvt). Google if he uses calendar. Connect X: click Connect, sign in as his X account. The app secret is already on the project; this step is the user token. If it fails, stop and tell the Ventari Team — do not paste a secret in chat.
        2. Sending (optional on this call). If he has a Resend account, paste his API key on the Sending card. If he does not, skip. Mail from the CRM will not send until that key is his.
        3. CRM / Contacts. New company is on the CRM list. Every contact row opens a card: name, email, phone, company optional. That card is live on v23. Rows that say "Unnamed contact" / "No company yet" are GHL people with no name — add whatever you have; you do not need a company first. Contacts that already have a company keep their email and phone on the person. Real companies from the cutover still open on /crm/clients.
        4. Messages. Social DMs is X/Instagram/Facebook, not his GHL inbox. His GHL threads are not in this CRM yet. Do not promise they will appear this week.
        5. Ask Executive (the bubble in the corner) is Ventari's assistant. It must not appear on his CRM. If it does, that is a bug — refresh; v23 hides it by pack. Do not demo it.
        6. Do not change GHL, cancel GHL, or point his website at the new CRM. The replacement list is in §13e; the Ventari Team says go on form intake first.

        What to tell him, one sentence each: this is his CRM; GHL still takes the website form and the booking widget until we replace them one at a time; Connect X today if he wants Social on; Resend when he wants the CRM to send mail; Ask Executive is a later product if he wants an assistant on his own key.

        Client CRM Factory · Current operations

        Operating principles

        Full specification textdetail for agents and careful review

        Said three times on the 2026-09-14 call: the app speaks agent-language to clients (Markdown fragments, JSON words, "control room", "UNASSIGNED is triage"). Every client-facing string reads as a sentence a business owner understands without a glossary, in the client's words from the pack where the pack has them. This is the token contract's neighbour: a copy contract. Recorded here so it is not lost. 9g landed; the copy pass itself is still an open lane. The Ventari Brain's caveman skill is the same rule for agents talking to people.

        What a client owns - a sentence, recorded 2026-09-19

        The line "you own the code" was said to two prospects on three calls in one week (2026-09-10 and 2026-09-17). It is not true and cannot be made true: Ventari LLC owns the CRM interface, templates and modules (Technology Ownership agreement, IP section), and a pack is cloned in an afternoon, so "we customised it, it is useless to anyone else" no longer holds. The sentence to use instead, in the client's words: you own your instance, your data and your accounts, and the right to keep running it. A client owns its Vercel and Supabase projects (§5 item 4), its data, its content, and holds a licence to run its instance and the skills bundled or bought for it. It does not own the source, the factory or a module. When a client asks for the repository, that is the answer.

        Beeem ownership exception, 2026-09-24

        Its README names the database as Ventari-owned, its repository sits under joyblisscoder, and its production deployment is on that account's Vercel project. That can be an approved managed-service model, but it is not evidence that the client-ownership rule above has been met. Transfer it, contract it explicitly as managed, or migrate it to client-owned projects before using the standard ownership sentence for that engagement.

        Client CRM Factory · Current operations

        Decision register

        Full specification textdetail for agents and careful review

        This is a human-owned decision record. Agents may gather evidence and propose a dated correction, but they do not reopen, replace or invent an executive or client decision. Each current item belongs in one of four states:

        The numbered record below preserves the dated reasoning. A line marked Decided is history and authority, not an invitation to decide it again. When the summary and an older line differ, the dated correction in this section or §4b controls.

        1. Is Phase 9 acceptable as written, meaning Nick's migration is done by hand and recorded rather than automated? Decided by doing it: yes. It is slower, and it is the only way the later phases got honest inputs.
        2. Where does a skill body live?
          Answered 2026-09-19 by reading the code (Alex). Decided. A skill body is Playbooks/<skill-id>/SKILL.md in the Ventari Brain vault. The app reads the vault through lib/vault.ts: in production over the GitHub API from joyblisscoder/VentariBrain (the GITHUB_REPO_OWNER / GITHUB_REPO_NAME defaults, with GITHUB_TOKEN); locally from VAULT_LOCAL_PATH. scripts/generate-skill-catalog.ts --vault scans Playbooks/**/SKILL.md and Playbooks/_capabilities/*.md and writes _modules/skill-catalog.json; loadSkillCatalog() reads it, with a legacy-index fallback for playbooks not yet on canonical frontmatter. Agents resolve skills through that catalog (lib/agent-tools/execute.ts, lib/build-lane/skill-subagents.ts, the executive); the Agents tab renders it; per-client selections live in client_skill_profiles. Lessons are appended to Playbooks/<id>/SKILL.md by lib/skills/lesson-writer.ts; Playbooks/ is hard-blocked to every other write. Checked 2026-09-19: VentariBrain on GitHub carries Playbooks/ (39 entries) and _modules/. The client half, and Phase 13f. A pack binds skill ids (skills: [{ id, enabled, requiresCapabilities }]), but a client deploy carries no vault, no catalog and no vault token - nothing in the deploy action or the client-deploy verifiers touches them - so there is nothing to resolve a binding against. That is why Rising Origin's crm_intake_guide is enabled: false. Design: (a) at deploy, the action, which runs on Ventari's side with the vault token, reads the pack's bound skill ids and copies exactly those bodies and their capabilities into the release as a read-only bundled catalog; lib/vault gains a bundled source for client packs. (b) The agency instance exposes a skills endpoint that serves versioned skill bodies to client instances against a per-instance licence; a client's shop surface buys a licence row on the client's own database and the instance fetches and caches the body. (c) The vault is never mounted or tokened on a client host - it holds every other client's data. (d) A client's agents write lessons to the client's own ops_skill_versions, never back to Playbooks/. (e) The client holds a licence to run bundled and purchased skills; a module is not owned (§4c). Phase 14's definition follows: a skill is a Playbooks entry with canonical frontmatter; an install bundles what the pack binds. Single source: VentariBrain/Playbooks on GitHub is the source the app reads and writes; the catalog should be regenerated on push rather than by hand. The separate ventari-playbooks repository (Suzuki Brain registry, 2026-08-27) was a backup from when VentariBrain had no remote and could not be cloned on Windows; both reasons are gone, and it is retired as a source.
        3. Does the unbound host fail closed, or is the agency default deliberate? Decided 2026-09-14: fail closed. An unknown host gets a plain not-configured page (404) before auth or data. localhost / 127.0.0.1 still resolve to the agency pack when NODE_ENV !== 'production'. 4b. A surface is on when it is his, not when it is visible. Decided 2026-09-13: Phase 9d, two lanes scoped and dispatched the same night, before the design pass.
        4. Who owns a client's database project?
          Decided 2026-09-19 (Alex): the client. The client owns its Vercel project and its Supabase project; Ventari is a collaborator, drives deploys through the deploy action, and has no Git integration on the client's project (Phase 13a, hosting half decided 2026-09-12). Rising Origin's fact pattern is the rule. The runbook no longer branches. The seat is the client's and stays the client's (2026-09-20): the collaborator identity a client provides (Rising Origin: ventari@risingorigin.com, a mailbox on his domain) is never a member of a Ventari team, never linked to a Ventari GitHub login, and never invited to anything but that client's projects. It was found holding all three, and another client's repository name had reached his mailbox through it; cut back the same night. The instance secret key (INSTANCE_SECRET_KEY; GOOGLE_CALENDAR_TOKEN_KEY is the older name) is generated and written to the client's password-manager entry in one step, and marked Sensitive on Vercel so a wrong paste is refused rather than silently accepted; it encrypts every stored connection secret, and losing it means re-entering them all (Rising Origin, v25).
        5. The token contract in Phase 9c. Decided 2026-09-11: the proposed table, with names aligned to the keys already in code. Typography roles carry transform and tracking. The contract lane and the extraction lane were scoped the same day; the six conversion lanes follow the contract lane.
        6. Messages and Client Success on Rising Origin's pack. Corrected 2026-09-24: Messages (inbox) is enabled and becomes useful when he connects a mailbox. The duplicate /clients roster (client_view) is off because he has no client-health pipeline. A future Client Success module is an explicit optional post-sale workspace, not a second CRM.
        7. Attribution for him. Corrected 2026-09-24: the shared analytics bridge plus optional GA4 and Meta measurement are in production (introduced in v34 and carried by current v37). The pack declares GA4 and Meta capability, while first-party website measurement remains off and the lead-source entitlement is not enabled. Connection setup and an end-to-end visitor-to-booking-to-revenue proof remain rollout work. Provider traffic metrics stay separate from CRM attribution.
        8. Contacts joins the core as a pack-declared surface. Decided 2026-09-11: a surface joins the core when a second client would plausibly want it. Off for the agency, on for Rising Origin, a view over existing data, no model. Audits do not: his five audit rows become organisation notes and he is told.
        9. Does a CRM buyer get Ask Executive, and on whose key?
          Decided 2026-09-19 (Alex, with Cody): yes, on the client's own key. Corrected 2026-09-24: key-paste is the standard built mode. The client stores its own provider API key - from the client's own OpenAI or Anthropic developer account, billed to the client - on a Connections card (kind ai, encrypted like a mailbox password); the instance calls the model server-side on that key. Host-connect is parked bespoke work, quoted and built only on request: the client's own machine runs the provider CLI signed into the client's own subscription and registers as a host through the existing lib/agent-connections host protocol. It works while that machine is awake - the trade the client makes for using a subscription. Claude sign-in completes inside the native binary on the client's machine; Ventari never collects or proxies a session (lib/agent-connections/providers.ts). tenant-model-gate remains: Ventari's keys are never used on a client deploy by default. Ventari-metered usage - Ventari's keys, rebilled as the passthrough - is the Helix tier's model, chosen in the customer agreement (Technology Ownership agreement, "AI usage is separate from maintenance"), and is not a card on a CRM-only instance until its price and policy are approved.
        10. 13e form intake. Decided 2026-09-19 (Alex; Cody committed the path to the client 2026-09-02): yes. Rising Origin's website form posts into his CRM. The named next build after 9f.
        11. Automations, including auto-email on a new deal
          Decided 2026-09-19 (Alex): not a default - a client-configured rule. The Automations area (app/(dashboard)/automations/: builder, rules, events, forms, funnel, runs) is a pack-declared org-workspace tab ('automations' in ORG_TAB_IDS). A client pack turns the tab on; the client builds its own rules - an auto-reply to a new lead, a notification to its team, or nothing. Nothing sends until the client has a rule and its own sending key on Connections. Rising Origin's pack does not yet declare this tab. Approved by Alex 2026-09-24: enable it for Rising Origin now that Resend is live; implementation remains a pack and release change.
        12. Native booking for a CRM buyer
          Decided 2026-09-19 (Alex): yes, as a pack-declared surface over the agency's existing booking, writing to the calendar the client's users connected. Calendar access comes from users authorising their own Google accounts through the instance's own OAuth client (item 13). Never hard-code a client's calendar. For Rising Origin, 13d already mirrors his GHL calendars; when he moves his booking widget off GHL is his decision under 13e, not a build decision. The shared booking surface and seeding path now exist; his calendar connection, seeded route, public-link switch and one real booking remain the rollout proof.
        13. Google sign-in and Google access per instance. A rule, 2026-09-19 (Cody): one OAuth client per client instance, created under Ventari's Google Cloud account and named for the client, so a breach is traceable. Never the agency's client. Rising Origin's OAuth client exists and is used by its Google connection and sign-in paths.

        Client CRM Factory · Current operations

        Evidence record

        Full specification textdetail for agents and careful review

        This section is a dated evidence record, not the current status ledger. Later checkpoints correct earlier tense without erasing what the walkthrough proved. Use §4b for current state and retain these examples as acceptance criteria.

        Added 2026-09-10. Alex opened a running client instance and found, in a few minutes, a class of problem the whole apparatus had missed. It is recorded here because it changes what "done" means for every phase below.

        The pack was right and the page was wrong

        Rising Origin's instance rendered its own name, colours, logo, navigation and pipeline, and still served Ventari's favicon, Ventari's apple-touch icon, Ventari's share card, and a web manifest offering "Install Ventari" on the client's own domain. Six brand files sat in app/ as Next.js file conventions, which Next serves for every host regardless of the loaded pack.

        verify-client-surface-leak reported clean throughout, correctly, because it resolves pack surfaces and a file-convention icon is not one. The check's green was narrower than it read, and this programme had been quoting it as though it covered the page.

        Fixed, with a new check (verify:no-global-brand-assets, manifest now 74) whose third assertion is the durable one: every URL the document head actually emits must be inside the loaded pack's own directory. Its failure messages name the pack field to use, so the rule explains itself to the next agent who trips it.

        Also found by looking, and still open:

        • 'Ventari Executive' is hardcoded in six places including an aria-label, so every client's agents tab presents Ventari's executive as theirs. The fix is install-time agent binding, not a rename: a Ventari agent wearing a client's name hides the problem.
        • The lead operating desk renders tenant_id to client users as "ventari (fixed)". The value is correct; showing it is not.
        • Stewardship, Team and Kickoff are navigation-contract questions rather than bugs. Team populates with Ventari's people.
        • A client's favicon was a hand-drawn letter F, a placeholder copied out of their own website because a brief named the path and nobody opened the file.

        The rule this produces, and it belongs in the skill: a brand asset named in a brief is verified by looking at it. A file path is not evidence of what the file contains. And a phase is not finished until somebody has opened the page.

        9h.3 - the model in the Ask panel (designed 2026-09-21, parked)

        Show which model answers and let an admin switch it without re-entering the key

        Panel line on a client instance: "Assistant · <provider model> · your key", from the ai row. Admin-only dropdown (the card's GATEWAY_MODEL_CHOICES plus Other) → PATCH /api/connections/ai { model }, same id validation as the card, gateway keys only (an Anthropic key has one model), effective on the next turn. Team members see the label. verify:ai-card gains the check. No migration. FINDINGS 29.

        What the pack gate is, and is not (recorded 2026-09-21, Alex's question)

        A client cannot unlock modules on their own through anything we ship: the pack (lib/tenant-packs/&lt;packId&gt;.ts) is in our repo, compiled into the bundle on our runner, and released as built output to their Vercel project. No env var, screen or file on their side flips an entitlement; a change is a PR, a checksum move with a reason, and a release.

        The gate is navigation and surfaces, not absence of code: every build carries the whole app (the deploy prunes cron routes only - "gated is not absent", scripts/deploy-client.mjs). And the Vercel project is theirs: an owner can download a deployment's files, edit the minified bundle and redeploy it with the CLI. That is tampering with a build, it breaks on the next release (instance-identity assertions, latestMigration, host binding) or is overwritten by it, and it unlocks tabs that do nothing without what only we hold (a Brain, our roster, our lanes, our credentials). Mode 1 means they own the instance; the last line is the agreement - "a client owns the instance, the data, the accounts, not the source" - not the code. Do not describe the pack as a licence lock.

        Phase 13g - Website analytics standard (v32, corrected 2026-09-23)

        Full specification textdetail for agents and careful review
        Decision (Alex, 2026-09-21, confirmed for this lane 2026-09-23): standardise the setup that already works

        Every client instance that has a Vercel website gets website reporting the same way: the pack declares the provider, an admin enters the token on the existing Connections card, Search Console stays on its own card and property, and CRM bookings and revenue stay labelled as CRM outcomes. The operator procedure is docs/runbooks/growth-reporting.md.

        • Pack contract

          integrations.analytics: { provider: 'vercel' | null, projectId, teamId }, required-and-explicit on every pack. vercel is the only website-analytics adapter: a site hosted on Vercel with Web Analytics on. A pack that declares null has no Website analytics card, and POST /api/connections/analytics answers 404. Rising Origin declares vercel; the project id is entered on the card until it is copied into the pack. This describes the v32 provider already shipped; the complete factory standard also requires the separately labelled GA4 adapter in Phase 13g.1.

        • Connections card "Website analytics"

          (kind analytics, migration 20260925190000, already the connection row): admin only (requireAdmin). One card on /settings/connections. The client's own Vercel token is verified by reading the project once, which also records whether Web Analytics is on, then stored encrypted. It is never shown again and never read from env on a client. The project id comes from the pack when declared, otherwise from the card. Six labels are derived when the card is read (websiteAnalyticsPhase). They are not written to the row. Stored status stays connected, error, or disconnected.

          A saved token is setup. A count is observed data. The card shows one next step and does not repeat the SEO & GEO report. Disconnect is DELETE /api/connections/analytics and clears the secret.

        • Windows

          Day-or-wider Vercel queries already send a Pacific calendar date (apiDate, America/Los_Angeles) to both the count and the series. This branch does not replace that helper. The new Connections presence count (probeWebsiteAnalyticsPresence, precise: false, 30 days) uses it. At 2026-09-23T06:30:00.000Z that request is date-only since=2026-08-23 and until=2026-09-22. A UTC date of that instant is 2026-09-23. Growth "Last 24 hours" stays an exact rolling window: granularity hour, precise ISO since and until, hour labels in Pacific time. A custom range of two Pacific days or less is also granularity hour with precise ISO bounds. At that instant, 1 September 2026 is since=2026-09-01T07:00:00.000Z and until=2026-09-02T06:59:59.999Z; 1–2 September 2026 is since=2026-09-01T07:00:00.000Z and until=2026-09-03T06:59:59.999Z. The freshness line names that range. It says the exact last 24 hours only for the Last 24 hours preset. Day, week, and month chart labels use the timestamps the reader returned. Hourly unique visitors are not summed into the period unique total. A failed or unreadable read stays unavailable. A ready snapshot whose whole-site visitor or page-view total is null stays unavailable on the report as well, and is not labelled Website reading. Zero visits and zero page views is an observed empty window. The presence read does not rewrite the connection row. No live Vercel call was made for this correction.

        • Reader

          lib/growth/analytics-for-instance.ts: the agency reads env (VERCEL_ANALYTICS_READ_TOKEN and VERCEL_ANALYTICS_PROJECT_ID; a pack projectId wins when declared). A client reads its connection row and never the agency env. A client with no row is told to connect on Settings → Connections. A missing read stays empty rather than a filled zero from another instance. The SEO & GEO cached read is keyed by pack id, never by the token. Cache window 900 seconds. The freshness line names the window the reader used and the Pacific check time. It says the exact last 24 hours only for that preset. Any other hour window is named as its own Pacific start and end.

        • Search Console

          Totals are the pack property (searchConsole.propertyUrl), finalized Pacific days. The overview end day is three Pacific days before the Pacific day of the read; more recent days are still finalizing. The default overview is 28 days. Domain and URL-prefix properties are not substituted. Query/page history is the latest stored snapshot for that property (gscDemandPropertiesForPack), not a live inventory of every search.

        • CRM outcomes. Bookings are dated CRM appointment records. Commercial signed, paid, and revenue totals stay hidden until agreement and payment records cover the cohort. A website visitor, a search click, and a booking are separate measures.

        • Standard client crons

          STANDARD_CLIENT_CRONS in scripts/deploy-client.mjs is daily, because a Hobby project allows daily crons only. The reporting job is /api/cron/gsc-demand-snapshot at 20 6 * * *. A client deployment receives that list plus the pack's own crons when a person publishes an approved client release. Until that release, the running client keeps the crons from its last release. CRON_SECRET stays on the client env checklist. This correction adds no cron and no migration.

        • Per-client schedule override

          An entry in a pack's crons list in deploy/clients.json is a path string or { "path", "schedule" }. A string takes the schedule in the root vercel.json. An object uses its own five-field schedule for that path. The path must start with /api/cron/. A bad entry is a clear error and the deploy refuses. The override exists because the root file runs the rule tick every 15 minutes and a Hobby project allows daily crons only. The client pack's entry sets the rule tick to once a day. No other entry has an override, so no other client's output changes. The root schedule is not changed. The script cannot see the client's Vercel plan, its cron count limit or how exactly Vercel times a daily run.

        • Release

          Clients do not follow main. Production for a client is a published stable Release under §13a.1 (.github/workflows/client-deploy.yml, no push trigger). workflow_dispatch can only preview and the deploy retains the existing client-environment reviewer. This lane has not published a tag, has not applied a migration to a hosted database, and has not changed a live website.

        • Verifier. verify:analytics-card covers the six card states, the Pacific-day presence count, unavailable versus zero, the per-instance reader, and the standard cron list. Card verifiers stay structurally exempt from no-client-material.

        Not built, logged: Threads has a tile and no card (Meta pattern, when a client asks); the Executive in Team Chat on a client key. Do not start Phase 14.

        Phase 13g.1 - Core analytics bridge and selectable reporting modules

        Decision (Alex, 2026-09-23): install the safe bridge with core; activate measurement and verticals intentionally

        The factory must not rebuild an analytics stack for every client, and it must not force every client to run every kind of analytics. Shared core owns one inert attribution contract. Provider adapters, first-party measurement and business-model dashboards attach to that contract as declared modules. A merge to main still changes no client: existing instances move only through their explicit migration and pinned release, and measurement begins only after a site is connected and verified.

        The layers are:

        1. Analytics foundation (core, always installed). Tenant and program identity, site timezone, exact window semantics, unavailable-versus-zero, source vocabulary, evidence freshness, idempotency, module health and the shared report contracts. It stores no browser event merely because the CRM was deployed.
        2. Attribution bridge (core, always installed and dormant). One stable contract can link an anonymous first-party visitor to a CRM person after a trusted successful intake, native booking or authenticated identity event. It preserves first touch and separately labels session/latest touch. It reads existing CRM client and successful-payment truth; it never creates a second person, booking, pipeline, client or revenue ledger. A failed link is best-effort and cannot roll back or delay an operational write.
        3. Capture modules (pack selected). website_measurement supplies the root tracker, same-origin forwarder, server-minted first-party visitor and sliding session, page path, normalized referrer, five UTM fields, engagement and bot rejection. No site is registered, no cookie is set and no panel is exposed while the module is off.
        4. Provider adapters (independently selected). Vercel Web Analytics, Google Search Console and GA4 remain separately labelled evidence. Each has its own Connections state, credential scope, freshness and report panel. Their visitors, users, sessions, clicks and impressions are never summed or silently substituted for first-party identities.
        5. Outcome adapters (core contracts, source-specific readers). Intake, booking, held-call, proposal, client, successful payment and collected revenue map into the shared funnel only from their canonical operational sources. Attribution coverage always names linked, source-known and unattributed records. A missing join is not zero revenue.
        6. Vertical reporting modules (pack selected). Business-model dashboards consume the foundation and operational truth without forking the core. community_learning may add DAU/WAU, watch sessions, cohorts, retention, drop-off and at-risk membership. recurring_revenue may add active MRR, churn and renewals. They do not activate on a marketing CRM merely because their code exists, and member names or emails do not enter the generic analytics spine.
        7. Data quality and health (core, always installed). First-party and provider observations keep their source, window, freshness and completeness. Reconciliation may name reconciled, pending, review or unavailable; it never overwrites one source with another. Connection health requires a real scoped read, and a silent or failed interval never becomes a measured zero.
        8. Privacy and lifecycle (core guardrail, no consent product). Browser measurement contains no direct contact fields. Visitor rows have one fixed 400-day maximum lifetime matching the first-party visitor cookie, idle sessions expire after 30 minutes, and a service-owned erase operation can remove one visitor or every visitor linked to a person. Its append-only receipt names site, organization, program, erased time, and event count, and excludes visitor/session hash, person id, path, referrer, UTM, and cookie. This adds no consent banner, consent mode setting or client-facing privacy workflow.
        9. Installation and operations (shared control plane). Every module declares dependencies, owned routes/components, migrations, Connections requirements, dashboard panels, pack flag, health states, verifiers, release order and rollback. Connections explains setup and health; Growth reports evidence. Build-time pruning may later remove undeclared module code under Phase 13c, but activation never depends on hiding UI alone.

        The dependency direction is one-way:

        analytics foundation -> website measurement -> attribution bridge -> vertical reports

        Provider adapters attach to the foundation and can operate without first-party measurement. Outcome adapters attach to the bridge and canonical CRM records. A vertical may depend on the foundation, bridge or its own domain records, but core never imports a vertical. This keeps learning portable for future factory agents and prevents a community dashboard from becoming a hidden dependency of a normal client CRM.

        Connection and activation contract

        A new CRM contains the foundation and bridge schema. Existing instances receive additive schema only through an approved migration. All packs declare every analytics capability explicitly; the default for capture and vertical modules is off. An admin then connects the applicable provider, registers the allowed website origin, installs the same-origin tracker on an exact preview of the current site, verifies a real event, and only then exposes first-party reporting. Provider health does not make the first-party tracker healthy, and first-party health does not make a provider healthy.

        First-party truth contract

        A cookie-capable browser receives opaque, server-minted first-party visitor and session identifiers; raw cookie values are not stored. When cookies are unavailable, a valid page view may be stored as explicitly sessionless, with no invented visitor or session and no cookie attempt. The tracker sends pathname, normalized referrer, the five allowed UTMs, engagement and an idempotency id only. It sends no raw IP, raw user agent, arbitrary query string, email, phone or message. Analytics failure returns harmlessly and cannot block rendering, navigation, intake or booking.

        GA4 contract

        GA4 is a required capability of the factory analytics standard and an optional connection on each client pack. It is not the bridge and is not a prerequisite for first-party attribution. Its dedicated Connections card uses read-only Google authorization and an explicit property selection. Its separately labelled Growth panel reports GA4 users, sessions, page views, landing pages, source/medium, campaigns and key events. Search Console keeps its search-result evidence; Vercel keeps its hosting evidence; GA4 keeps its own measurement definitions. The factory standard is not complete until the GA4 adapter, card, property selection, honest freshness states and verifier exist, even though a client may choose not to connect it.

        Required verification before the first client enablement

        Prove tenant isolation; origin binding; idempotent route views; stable visitor and sliding 30-minute session behavior; cookie-disabled sessionless views; frozen first touch and separately labelled latest touch; direct, UTM and referral buckets; bot rejection; timezone boundaries; unavailable versus measured zero; trusted person linking; collected-revenue attribution; no PII in analytics; bounded retention and unlinkable erasure receipts; and failure isolation from intake, booking and payment. Then prove each enabled Connections card and panel in a production-like preview. A provider adapter, vertical module or public-site install gets its own reviewable change and does not widen the bridge silently.

        Current state on 2026-09-24

        Phase 13g's Vercel, Search Console and CRM outcome panels remain the shipped provider standard. The analytics bridge, GA4, browser Meta Pixel, and optional Meta Conversions API are on main (pull request 966, merge e89a8eec). Pack flags stay off unless a pack declares them. Rising Origin exposes GA4 and Meta Pixel as capability flags only. Those flags do not install a tag and do not enable Conversions API. Settings > Connections shows separate first-party, Vercel, GA4, and Meta rows and does not add a generic Analytics destination. The agency pack leaves GA4, Meta Pixel, first-party measurement, Vercel analytics, and the assistant key off, so those cards do not render on my.ventari.media. That is the pack. SEO & GEO keeps its name and route and shows one GA4 or Vercel trend at a time. Social Media opens on Overview. Purchase comes only from a succeeded provider payments row. Manual receipts do not create one. Vertical reporting modules stay unselected. CAPI stays inactive until an explicit enable on a reviewed connection.

        Money leads with a recent-charges chart (pull request 971, merge 3b27c770). Days follow the device clock. A day below zero means refunds were larger than charges, and that part of the line is yellow. The picker filters the loaded Stripe page. It does not fetch older history. Rising Origin tag client/rising-origin/v34 is that commit and was published. GitHub deploy 35943318530 reported success, including smoke. A logged-in look at Money and SEO & GEO was not completed. The public URL showed only the sign-in screen.

        The green Website and CRM chips, and the timeframe picker on the traffic trend, are pull request 972 (merge 2d76cf15, tag v35) and are carried by current v37. The change moved the controls clear of the plot. The separate mobile usability finding remains: the graph itself is too small. Its exit is a 390px logged-in screenshot with a materially readable plot, useful height and width, compact ticks and the existing timeframe control.

        Meta reconcile, delivery, and retention are in vercel.json. A client release still ships only the cron paths in deploy/clients.json. Rising Origin, Cymatica, and Myth & Ethos add none of these. The jobs do nothing useful until CAPI is enabled, and retention needs the outbox table.

        Migrations, 2026-09-24

        20260928142000_meta_capi_delivery_outbox.sql is applied on the agency database qekmtrshsuhrtrvjxyqz and on Rising Origin aoylknoetehsmghkbchk. Each verify showed one history row and the expected pack. The agency full apply at 01:15 UTC then recorded 20260922150001, 20260927140000, 20260927150000, both files for 20260928120000, both files for 20260928130000, both files for 20260928140000, and 20260928141000, with no error. Rising Origin's 01:20 UTC plan was identity-ok, 385 applied, 16 pending, 3 agency files skipped. The apply command was given. No completion log was pasted. core-stability / verify:migration-order is red on main because those three version prefixes each have two SQL files. Do not rename an applied file. npm test still stops at the older verify:client-roster-truth failure. verify:pipeline-lifecycle still fails on Windows because package.json uses a Unix NODE_OPTIONS prefix.

        X connection diagnosis, 2026-09-24

        The live reproduction superseded the earlier callback-URL inference. Consent completed, the production callback ran and returned HTTP 307 to /socials, but no connection persisted. PR #979 then repaired the OAuth token exchange, callback diagnostics, persisted status and focused verification. It merged at 91eb3cae and shipped as Rising Origin v37; the deployment smoke passed. The remaining production exit is a fresh authorized reconnect whose readback shows the durable account and a truthful connected card. Until that receipt exists, the source repair is shipped and the production connection is unproved.

        Deploy, 2026-09-24

        Vercel Hobby blocks a private-repo upload when the checkout still contains a commit whose author is not the project owner. vercel build was reading .git before the cleanup step. Pull request 973 deletes .git before the build. Tag v36 points at that merge; current v37 contains it and deployed successfully.

        Closed branches and remaining work

        Pull requests #969, #974 and #975 closed without merge. They no longer own files or block a fresh preflight. Useful findings were reconciled into this checkpoint; none of their unmerged changes may be described as shipped. The Today/client-home decision remains pack-scoped, Connections presentation can be reconsidered in the ordered surface pass, and collected revenue on the SEO strip still prints cents as a plain number. That last defect was seen in a sample preview and is not fixed.

        CRM Surface Polish Program checkpoint, 2026-09-24

        Full specification textdetail for agents and careful review
        This is the shared execution map for the current polish program

        It records what is cleared, what is blocked by ownership, and what stays deliberately separate. A green visual row in the generated spec means verified or complete; it never means active, preferred or merely healthy-looking. Amber means a real blocker or required attention. Future work remains neutral.

        Full-row accenting is semantic

        The generated table accents the entire row, including its hover and keyboard-focus state, so status can be scanned without turning isolated badges into decoration. Verified/completed uses the signature green wash and edge, blocked/attention uses amber, and queued or held rows stay neutral. The hover changes surface and edge together, settles immediately, does not move a child inside a clipping cell, is not required on touch, and is disabled by prefers-reduced-motion. The Suzuki Systems Brand Guidelines own the reusable rule; this table consumes it.

        Wave B · design contract

        • Pipeline opens on Funnel. The sequence is Funnel, CRM, Leads, Proposals, Audits, Kickoff, Stewardship unless a pack removes an undeclared surface.
        • Growth/SEO puts the traffic graph before large warnings. A warning belongs in the section whose evidence failed or below the primary graphic. Timeframe controls use the compact labels 24h, 7d, 30d, 90d, All, Custom and reuse one existing picker.
        • Social puts account identity first, then Reach for the last 7 days, then a compact single-row metrics strip where width permits. Instagram receives the same title hierarchy as Facebook. Counts may abbreviate (13.4k) while an accessible title or detail exposes the exact value.
        • Social and Money share the restrained chart language: continuous line/area, subtle bright-blue point markers, useful hover/focus detail and no decorative animation. Markers are opt-in shared behavior, not a global chart rewrite.
        • Delivery, Automations and Shop each receive one first screen: one lead, one chart or strip from existing evidence, then supporting material. Autopilot stays where it is. This pass does not restyle unrelated surfaces.
        • Connections gives every declared capability one operator-ready card. Remove redundant How-to sections when a connected card has no remaining setup or repair value; retain instructions where renewal, verification or recovery still benefits the operator.
        • A first screen tells the operator where they are. Today uses one short, pack-aware orientation sentence below its title: “Your follow-ups, scheduled work, and recent changes across Rising Origin.” It does not repeat the nav label or become a welcome essay.
        Rising Origin landing decision, implemented from 2026-09-24

        The global / route now reads its first-load destination from the trusted tenant pack. Rising Origin declares /crm/clients; the agency and other current packs keep their existing /dashboard default. An unbound host goes to /not-configured and never inherits the agency board. Rising Origin still has no client Today or Board surface: /command-center remains a staff operating console, not a finished client Today page. Today may become the home only after it has pack-safe evidence, the orientation sentence, useful empty states, and a 390px proof. Pull request 974's nav link does not by itself meet that gate.

        Wave C · responsive contract

        Every changed surface earns its own 390px evidence

        The Growth traffic chart must be materially readable, not merely free of overlap: give it useful height and width, then reduce tick and legend density without silently selecting a different timeframe. Status chips wrap into their own row. Primary-contact, deal and funnel text wraps or trails safely; a narrow card must not become an internal horizontal scroll box just to preserve desktop grabbers. The detail view remains the place for the full record.

        The Funnel stage rail is the intentional horizontal scroll container on a phone

        It needs a visible, subtle affordance—a clipped next stage or edge fade plus a concise “Swipe to see stages” cue—until the first horizontal movement. The rail has an accessible label, preserves touch scrolling and drag behavior, and never puts a second scroll box inside a deal card. The 390px acceptance screenshot must show both the cue and enough of the next stage to communicate direction; a technically scrollable but visually sealed column does not pass.

        Wave D · speed contract

        Growth currently waits for Search Console, Website analytics, the acquisition pipeline and optional GA4 before paint. Money waits for the Stripe ledger. The speed pass first measures provider and route timings, then streams or defers independent panels so useful local/fast evidence can render first. Connection health, provider totals and CRM outcomes keep separate loading and freshness states. A slow or missing provider never manufactures a zero and never blocks intake, booking, navigation or the rest of the page.

        Standard modules and client delivery

        • There is one canonical CRM spine. Client Success is an optional post-sale module for lifecycle, plan, delivery, health, reviews and outcomes—not a duplicate clients database. A pack activates it only when the operating model and data owner are defined; do not enable an empty workspace for parity. At sufficient scale it is a legitimate expansion/upsell.
        • Pre-sale intake stays accountless. After an approved accepted engagement, invite the client into one branded authenticated portal for decisions, files, structured intake and delivery status. The portal is an interface; canonical CRM, delivery, file and Brain systems remain the authority.
        • Community-only analytics belongs in selectable vertical modules such as community_learning, never in the core marketing CRM. Threads is likewise a separate future provider module: it currently has no client, OAuth, storage, card, pack flag or adapter and must not borrow Facebook/Instagram credentials by implication.
        • GA4 is part of the factory analytics standard but optional per pack. Its operator flow still requires trusted instance callback handling, read-only authorization, explicit property/stream selection, callback-result feedback, disconnect/repair and a dedicated provider panel. It does not replace Vercel, Search Console or first-party attribution.
        • Cymatica is outside this CRM Factory rollout. Sharing the agency pack does not make it a factory target. Historical pull-request titles are evidence of a past change, not current product truth; the client Executive's live GitHub status was intentionally reverted and must not be represented as shipped.
        Execution order

        Resolve the three overlapping PRs, re-fetch and repeat the full preflight, then run design, responsive and speed as separate reviewable changes. Run the functionality pass after those surfaces stabilize. The intake portal architecture remains a separately approved lane. No checkpoint here authorizes a push, merge, migration, release tag or client publish.

        The v30 walk, 2026-09-21: every tab, in the client's words

        Full specification textdetail for agents and careful review

        Alex walked the first client release with the 13e lane and the Assistant card live and opened every tab. Eighteen findings, all in docs/estate/FINDINGS.md under "The v30 walk on Rising Origin"; the ones that were code shipped in v31 (one PR, no migration). The two decisions worth keeping here:

        • A client keeps every tab, and every tab speaks in the product's words about what is and is not set up for them
          No teaser page, no hidden console. The Agents workspace resolves clientProduct once (CapabilitiesWorkspace) and each tab takes it: what an agent network, a team, a skill, a block, a run, a model choice is, and that none exists for them until Ventari sets up a Brain. The agency's rendering is untouched. What a client never sees: our repository, commit and file paths (Blocks), build-routing rows (Models), engagement gates (Client AI Teams), staff playbooks (skillsForClientInstance), the agency's SEO & GEO agent (Experiments).
        • Every read of a credential goes through the connection row, never the environment alone. Inbox (#914) and now Sent (resolveSendingApiKey) had read env; a client whose key was stored on Connections saw "not configured". The rule is already the sender's rule; the walk found the two readers that had not adopted it.

        Also from the walk: a contact can now be removed (softDeletePerson, soft, refuses staff and a deal's primary contact); an empty experiments ledger is the empty state, not an outage; Open in Chat is agency-only until the group-chat reply takes the client key (FINDINGS item 11).

        The pixel diff that proved nothing, 2026-09-11

        The token-contract worker built the snapshot harness, captured fourteen baseline images, captured fourteen after images, diffed them, and reported zero differing pixels on every pair. Its own finding 2 said every image was the "Checking your account" pending screen. Opening one image confirmed it. Two causes: the stack it pointed at was the fresh core build with two people and no deals, not the fixture; and the session cookie it minted carried a name the browser client does not read, so the app parked the user at /pending. A check that passes on identical inputs has not checked anything. The rule for every harness from here: the report shows the image, not the count, and a baseline of a page with no data on it is not a baseline. Sent back with the diagnosis; the conversion lanes do not start until the funnel shot shows deals.

        The same afternoon the operator registered check 75 in the manifest and not in the runner's allowlist, and CI went red before a single check ran. The runner's comments record that miss ten times; this is the eleventh. The pairing is one rule in two files, which is the finding.

        The harness earned its keep on the first conversion lane, 2026-09-12

        The shared conversion brief said text-zinc-500 converts to text-muted-foreground because the agency token is #71717a, which is zinc-500. Lane D converted it, captured, and the diff came back one blue channel unit off on every converted label: Tailwind v4 compiles zinc-500 to an oklch value that rasterises to [113,113,122], not [113,113,123]. The worker reverted the swap and reported the class as residue. That was a mapping the operator wrote from a hex table without rendering it; 1,788 sites would have shifted a shade nobody would ever notice by eye, on every Ventari page. The rule from Phase 9c holds exactly as written: the agency value is what the class renders to, and a check that measures pixels is the only thing that knows.

        Client CRM Factory · Current operations

        Security model

        Added 0.6, 2026-09-12, after a security review. Written to be stress tested

        The concern: Ventari will serve many different kinds of businesses from one application, and if that application is compromised or a change in it breaks, every client's CRM breaks at once. His proposal: a separate codebase per client. Alex's design: one codebase, a pack per client. This section says why the design was chosen, what it costs, where it is more secure than it looks, and where the concern is justified.

        Scope correction, 2026-09-24

        this threat model governs the canonical shared-core pack path. Beeem's standalone repository is an exception with its own patch and drift burden; its existence does not revise the decision below. It either remains an explicitly supported standalone product or is migrated back to the shared core. It must not silently redefine the factory standard.

        The two guarantees the design must give, in plain words

        1. A client can never change Ventari's codebase. A client never has write access to the repository. What is theirs is data: a pack file, a database, a Vercel project, a domain. A client who needs a code change that Ventari would not ship to everyone is a bespoke build, priced as one, and leaves the factory. The pack is the only thing that varies per client, and a pack cannot execute.
        2. A change to Ventari's codebase cannot break a client without a person choosing to ship it to that client. A client instance is built from a pinned release tag and moves only when a named person approves a new one for that client, in that client's GitHub Environment. main can be on fire and every client stays on their last approved release.

        Both are properties of the deploy model, not of how many repositories exist.

        Full specification textdetail for agents and careful review

        The three options, side by side

        Where it is more secure than it looks

        • Clients do not run main. They run a tag a person chose for them. The common picture of "push to main, every client updates" is not this system; it was never built that way and the deploy action refuses to work that way (no push trigger, planted and proved red).
        • Every client is already isolated where it matters: their own database, their own Vercel project, their own secrets, their own domain. A compromise of Ventari's Supabase, Ventari's Vercel, or one client's keys reaches nobody else. That is the estate map's whole result and it holds.
        • Landed 2026-09-14 (5c): a database names its pack (instance_identity). Pointing a pack at another instance's URL is refused at startup, deploy, and fan-out once the row exists.
        • A module a client has not bought is not just hidden, it is gated, and the gate is tested. The leak check resolves every pack's surfaces on every pull request; a Ventari feature appearing on a client instance is a failed build, not a support ticket.
        • One codebase is the only layout in which "patch every client tonight" is possible. Separate codebases turn a one-hour fix into a week of hand-merges, and the client whose copy drifted furthest is the one who stays vulnerable.

        Where the concern is right, and what is done about each

        Full specification textdetail for agents and careful review
        • It is a monoculture. A bug that passes all 77 checks and gets approved for a client ships to that client, and to every client who approves it. Diverse businesses mean diverse ways to hit a bug the checks did not imagine. Done about it: releases are per client, so a bug surfaces on the first client to take a release, not all of them; the walkthrough in Phase 14's preconditions stays human; and the pixel harness now proves that design changes do not move the agency by a pixel, which is the class of change most likely to slip past a functional check.
        • Core changes serve Ventari's needs first. A change Ventari wants for itself lands in the code every client runs from. Done about it: the rule from Phase 7 that every change is conditional on the pack or it is wrong, enforced by verify:tenant-aware-shell and the leak check; and a client never has to take a release they did not ask for.
        • A shared dependency vulnerability hits everyone. True in every layout. Done about it: dependency scanning on main, and the one-codebase model is the only one where the fix ships to everyone the same day.
        • If the account that owns everything is compromised, layout does not save anyone. True. Done about it: required reviewers per client environment; environment-scoped secrets; and, for clients who want it, the instance repository in their own organisation so a different account holds their release decision. That is the middle ground and it is real.
        • Many kinds of businesses will want different things. True, and the design's answer is modules and packs, not forks. When a client's need cannot be expressed as a module, they are a bespoke build. The factory should say no to that client rather than fork for them.

        The gap the med-spa example exposes: gated is not absent

        Full specification textdetail for agents and careful review

        Alex, 2026-09-12: "when we build med spa CRMs that means we will have to build them first in the Ventari app." True, and it exposes the one place this section was soft.

        A pack gates surfaces: navigation, routes a user can reach, what the shell renders. When the med-spa modules are built, their code lands in the one codebase, and today it would ship in every client's deployment bundle, Nick's included. He would never see a med-spa page. But the API routes would exist on his instance, unlinked, and a vulnerability in one of them would be reachable there and on Ventari's own instance. "The gate is tested" answers the leak. It does not answer this.

        The answer is build-time pruning, and the mechanism already exists

        The deploy action (Phase 13a) already rewrites the build's vercel.json from a per-pack cron allowlist before vercel build, on the runner's checkout only. The same step removes the route and component directories of every module the pack does not declare, then builds. A client's deployment then physically contains the code for their modules and nothing else. The med-spa code on Nick's instance is not gated; it is not there.

        • Phase 13c, scoped: each module in the pack contract names the directories it owns (app/(dashboard)/&lt;x&gt;, app/api/&lt;x&gt;, components/&lt;x&gt;). The deploy script prunes every directory owned by an undeclared module before the build. A verifier builds the pruned tree for Rising Origin and asserts that no route for an undeclared module exists in the output, and that the committed tree is untouched afterward, the same way the cron step is proved.
        • The precondition is the module-to-directory map, which does not exist yet. Writing it is also the audit of which code is actually shared (auth, tenancy, the shell, components/ui) and which is a module, and that audit is worth having regardless.

        What building a vertical in the one codebase costs, said before vertical two

        • Every vertical is built as a module directory behind a pack, under the Phase 7 rule: agency behaviour unchanged, proven by the shell check and the pixel harness on every pull request. The rule scales; the checks run on each vertical's code whether Ventari uses it or not.
        • Ventari's repository becomes the platform repository. Its CI runs every vertical's checks on every pull request. That is time, not risk, and it grows with each vertical.
        • Ventari's own deployment goes through Vercel's Git integration today and carries every vertical's code. To be pruned like a client, Ventari's own deploy moves onto the same action with the agency pack. Decide before vertical two.
        • When a vertical's needs cannot be expressed as a module, it is a separate product with its own codebase, and that is a business decision, not a security one. The factory should refuse that client rather than fork.

        The controls, all small, all ahead of the first client deploy

        1. A GitHub Environment per client with required reviewers (Alex or Cody); the deploy job pauses until one approves.
        2. Secrets scoped to that environment, not repository-wide.
        3. Tag protection on client/*.
        4. The deployed release of every client recorded and visible: tag, date, approver. Rollback is Vercel's promote-previous-deployment.
        5. Dependency scanning on main.
        6. The 77 checks on every pull request, which a copy would not have.

        Why a client's Vercel project never gets the Git integration

        Full specification textdetail for agents and careful review

        The preferred workflow is Vercel connected to GitHub: previews per pull request, deployments traceable to commits. That stays exactly as it is for Ventari's own project, which is Ventari's team looking at Ventari's code.

        It cannot be used for a client's project, for one reason: connecting a repository to a Vercel project gives everyone on that project the deployment Source tab, which browses the deployed source files. In a client's account, that is the client reading Ventari's codebase. It would also require Ventari to authorise the Vercel GitHub App on the private repository for the client's account. So rule one of the deploy action is no Git integration on a client project, and the verifier proves the workflow has no push trigger.

        What that workflow is wanted for, and where each part lives instead:

        The honest caveat

        a prebuilt deploy uploads the build output, and the client project's Source tab shows that: compiled server bundles, not the readable repository. It is code, but it is what every deployed Next application exposes to its own hosting account, and it is exactly what the client's current CLI-deployed rising-crm shows today. Phase 13c pruning also means that output contains only the client's modules.

        What the GitHub controls are, since the word sounds like access

        These are the intended repository controls

        Current verification, 2026-09-24: the client-rising-origin environment exists and scopes the client deployment secrets, but the GitHub API reports no environment protection rules and no repository ruleset. The approval gate is therefore procedural today; it must not be described as an enforced reviewer or tag gate.

        1. An Environment per client (client-rising-origin) scopes that client's deployment secrets. Add required reviewers when the repository plan and settings support them; until then, release authorization remains a named human operating step outside the workflow.
        2. The client's secrets stored in that environment, so a workflow run in any other context cannot read them.
        3. A ruleset for client/* is still a desired hardening control. None is currently returned by the repository API, so only an explicitly authorized person may publish a production client release.

        Do not mark these controls enforced until a read-back proves them.

        The decision

        One codebase. Packs per client. Pinned, per-client, human-approved releases. An instance repository per client when the client wants to hold their own release decision, and it holds no application code. Never a copy of the codebase per client, because a copy is where fixes go to die.

        Client CRM Factory · History & evidence

        Implementation history

        Recorded because the table in section 2 is already slightly out of date, which is the point of section 7.

        Row 1 of section 2 got easier. A pack can now declare a logo, favicon and font stack, so writing the pack file covers more of a client's identity than it did this morning. It does not cover typography in practice: a declared font is named but never fetched, which is its own finding.

        At the time of this record, nothing else in section 2 had moved. The current-state table in section 2 records what changed afterward.

        Client CRM Factory · History & evidence

        Evidence rule

        Every count here is measured and cited, because six counts have moved under a document in this programme: 276 to 279 migrations, 19 to 71 to 72 checks, and 50 to 51 cron entries inside a single day. A figure quoted without a commit is already stale. Re-measure rather than trusting the table.

        System index