Map the business
We learn the people, tools, data, and work.
active working standard · Ventari Team
The operating standard for building, launching, and improving every custom client CRM instance
The simple story
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.
Every client starts with the same proven core. Their pack tells the core what to show, how to look, and which tools to connect.
We learn the people, tools, data, and work.
The pack holds the brand, modules, and rules.
We connect the database, test it, and release it.
A safe core update can help every client.
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
Step 1 · The client pack
It says which modules and tools this client gets. Nothing else changes.
Step 2 · The CRM core
The same for every client. The pack only decides what is switched on.
Brain and knowledge modules stay dark until a venture pack turns them on.
Step 3 · Their own CRM
Social reply guard is off in every pack today. Its production adapter has not been built.
Optional tools are available to any pack and are not part of this example.
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.
Every client’s records live in their own database.
A client moves only when their release is chosen and checked.
A proven improvement can be shared with every client.
Where the work stands
Still a draft: Phase 9i - Native social messaging flows (draft; not launch-ready)
What exists across the Ventari estate, how it is mapped, and what has shipped.
How a client CRM is built, installed, secured and delivered.
Nothing matches. Try a different word.
The program, in order
Fourteen phases across two documents. Choose a phase to open its section.
Document 1
Phases 1–8 · all landed · Version 1.6
Document 2
Phases 9–14 · Factory v0.11
Document 1
17 sections · Phases 1–8 landed · Version 1.6
Part I · Phases 1–8
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
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:
Every top-level and second-level heading is stable and citable. Implement 8.2
is a complete instruction.
Every substantive claim in this document carries one of four labels. Where a claim carries none, treat it as Design requirement.
Ventari Estate Map · Foundation
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.)
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:
Touches is directional and per feature. There
is no "if you change X, open these" index across the estate. (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:
Ventari Estate Map · Foundation
(Confirmed.) Three area maps in VentariFullApp, all in current use:
Scroll sideways to inspect the full table
| Map | Shape | Verified |
|---|---|---|
docs/calendar-control-plane/ |
Eight numbered rooms, _meta/, a CONTEXT.md per room, a dispatch table |
2026-09-07 |
docs/client-health/ |
CONTEXT.md, goal, roadmap, authority, policy, runbook, source contracts |
in use |
docs/work-ledger/ |
CONTEXT.md, goal, roadmap, actor identity |
in 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.
(Confirmed.) Three ways:
icm-architect skill is installed on the Ventari operating desk.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.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.
(Confirmed, 2026-09-07, via the GitHub API and committed files.)
Scroll sideways to inspect the full table
| Thing | Count | Note |
|---|---|---|
| Repositories under the account | 95 | Roughly 40 pushed in the last two weeks |
| Repositories in the audited checkout set | 11 | Point-in-time local census; not live or organization-wide |
| Hosted projects | 5 | Project names do not match repository names |
| Servers outside the hosting platform | 2+ | The audit engine, plus two executive hosts |
| API route files in the app | 514 | |
| Database migrations | 270 | |
Files under scripts/ |
941 | |
| Verification scripts | 800 | Files named verify-* at 1771e2f. 16 ran automatically; 17 since the vault verifier landed. Version 0.1 said 803, which also counted live-verify-* and purge-verify-* |
| Scheduled jobs | 51 | All under one app identity |
| Marketplace skill packages | 58 | |
| Environment variables in the example file | 112 |
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.
Corrected 2026-09-07. Cross-repository relationships are covered, by
_canon/SYSTEMS-AND-FEATURES.md. See section 2. What remains uncovered:
(Confirmed.)
Touches is per feature and directional, and no
index answers "if you change X, open these" across the estate.(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:
Scroll sideways to inspect the full table
| Artifact | Where | What it binds |
|---|---|---|
| North Star | Brain Reports/Ventari Software Evolution/ |
§4 is the sentence the map is measured against |
| Evolution program | same folder, and ventari-evolution.vercel.app |
§0a ambitions, §0b extraction doctrine, §12 pass criteria A1–A7 and H1–H9, §17 maintenance contract |
| Claim protocol | VentariFullApp issue #277 |
Claim files and branch in every repo before editing |
| Software registry, D17 | app lib/software-registry/ |
The census feeds it and must never duplicate it |
| Brain write rules | VentariBrain/CONTEXT.md |
Timestamp, attribution, never-touch list, required before writing _canon/ |
| Agent experience rules | _canon/AGENT-EXPERIENCE.md |
Conditions every agent reply meets, including the Executive once it reads the map |
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
(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.
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
verified_at and
verified_revision. A card can be out of date and still honest, because it
says what it was true against.stale.Ventari Estate Map · Foundation
Closed set. Anything that fits none of these does not belong in the map.
Scroll sideways to inspect the full table
| Type | Lives at | Carries |
|---|---|---|
region |
regions/<slug>.md |
A part of the estate: what it is, its boundary, its blast radius |
process |
processes/<slug>.md |
A movement across regions: input, movement, output |
index |
_generated/*.md |
Generated inventories. Never hand-edited |
contract |
any CONTEXT.md |
What a folder is, what it reads, what it writes |
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.
Scroll sideways to inspect the full table
| Universe | Meaning |
|---|---|
live |
In force. Implement and cite against these |
leftover |
Present, no longer the main path. Touch only if in scope |
dead |
Finished or abandoned. Do not implement against these |
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.
Ventari Estate Map · Foundation
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.
Scroll sideways to inspect the full table
| Layer | How | Why |
|---|---|---|
| Region boundaries and permissions | Written by hand, reviewed | Slow-moving, high-value, requires judgment |
| Blast radius index | Written, validated by script | Judgment, but checkable |
| Repository, service and server inventory | Generated | Changes weekly |
| Route, table, function and policy inventory | Generated | Changes daily |
| Scheduled jobs, egress points, environment usage | Generated | Changes daily |
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
Each phase names what it produces and one Exit sentence. A phase is not finished until its exit condition is demonstrably true.
Scroll sideways to inspect the full table
| Phase | Program row | Existing work it joins |
|---|---|---|
| 1 Census | VE-178 | Supersedes VE-003's scope, feeds VE-046 dead-framework notes and the D17 software registry |
| 2 Regions | VE-179 | New. Passes VE-174's test, whose wording replaces this document's "two hops" |
| 3 Blast radius | VE-180 | New |
| 4 Check audit | VE-181 | VE-033 covers the security-shaped subset; this is the wider audit |
| 5 Gate proofs | VE-182 | §12 already defines these as pass criteria A3, A4, A5, H9, anchored at VE-012, VE-152, VE-153, VE-169 |
| 6 Client core | VE-183 | Runs under D14 and D17, alongside VE-171. No second reusable-core track |
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.
live, leftover or dead.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.
Exit: a person or agent who has never seen the estate can name every live region and its boundary after reading one page.
Exit: for every region, the map names what a change to it touches, and the validator passes.
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.
Scroll sideways to inspect the full table
| Spec said | Measured | |
|---|---|---|
| Verification scripts | 803 | 824 |
| Running automatically | 16 | 34 |
| Security-named | 47 | 41 |
| Reachable by an npm script | — | 665 |
| Orphaned, no npm script at all | — | 159 |
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
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.
A classification nobody can check is an opinion. Two things keep this one honest:
_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.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.
Scroll sideways to inspect the full table
Measured at 1333ef9 |
|
|---|---|
| Policies parsed | 532 |
Gating on is_staff() with no tenant, org, person or client clause |
210 |
Policies anywhere referencing installation_id |
0 |
Already using foundation_can_access_tenant |
16 |
Scroll sideways to inspect the full table
| Wall | Mechanism | State |
|---|---|---|
| Between peer agencies | A separate database each | Physical, nothing to leak |
| Between clients inside Ventari | organization_id subtree |
Held in the test. Client B could not touch client A's tasks |
Scroll sideways to inspect the full table
| Model | What it is | Status |
|---|---|---|
| Rising Origin | Hand-built, in the client's own GitHub org, its own 14-migration schema | Shipped to a paying client |
growth-os → cvc-admin |
White-label template, forked, in joyblisscoder |
Shipped, "instance #1" |
lib/tenant-packs/ |
Config inside the shared app, the hybrid path | Not shipped |
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
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.
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.
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:
- shared_rls_isolated — one Postgres, many
tenant_idvalues, tenant-aware RLS. Rejected as the cutover strategy.- 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.
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.
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.
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.
, 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?
'ventari', the literals are harmless.
The tenant packs point this way: helix.ts declares
literalTenantId: 'ventari'.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.)
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.
Scroll sideways to inspect the full table
| Count | What it is | Method |
|---|---|---|
| 9 | Functions taking p_tenant_id with an explicit refusal of anything but 'ventari' |
Static scan. This is the number that prices the phase. |
| 22 | Functions whose bodies refuse a non-'ventari' p_tenant_id |
Runtime pg_proc after 276 migrations. Includes overloads and combined predicates |
| 483 | Public functions whose body merely contains the string | Runtime. File parse of dollar-quoted bodies gives 482. These other ~460 refuse nothing |
Scroll sideways to inspect the full table
| People in / out | 26 / 26, 0 fabricated |
| Leads in / out | 31 / 31 |
| Stage history in / out | 38 / 38, in order, 0 forged |
| Corruptions | none |
| Ambiguous identities routed to review | 6 |
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.
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.
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.
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:
'ventari' literal problem bite? 482 functions carry one. This
is the fastest way to find out and it decides everything downstream.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?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.
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 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.
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.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.
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.
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 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.
(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/.
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.
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:
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.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.REGISTERED_PACKS holds only
Helix and the agency pack, and host bindings only bind to registered packs.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.
Named 2026-09-09 by what stage 1.5 found. Not exploratory. Four items, each already located:
PIPELINE_KEYS.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.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.
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.
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.
Scroll sideways to inspect the full table
| Surface | Where |
|---|---|
| Sign-in wordmark reads VENTARI | components/auth/AuthShell.tsx line 72 |
| "Chat with Ventari" on every screen | components/command-center/ConversationPanel.tsx lines 173-187 |
| Funnel tabs read Organic / Affiliate / Client Health / Lumina Sales | FunnelControlRoom.tsx lines 83-88, and funnel/loading.tsx |
| Pipeline falls back to the agency pipeline | lib/crm/pack-pipeline.ts lines 47-50 |
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.
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:
New / Contacted / Qualified / Proposal / Negotiating / Won / Lost, named
Sales. Derived from a real client build; no Ventari vocabulary.ventari_sales is agency-only.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
Using the priority classes already in use in Ventari specifications.
df62cfa. Deletion left this
phase's scope the same day; see §8.4.)unproven and shared-RLS cutover as blocked. (Documented.)
If it is wrong once it is wrong in every client build.All three or none. A census without blast radius is a list, and lists are what the estate already has.
docs/executive-context.md. Blast radius is not part of this
read until Phase 3 builds it (12.3, superseded 2026-09-08).joyblisscoder and are already in scope and in the census.Ventari Estate Map · Operating standard
type, universe, status, verified_at, verified_revision.Ventari Estate Map · Operating standard
(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.
Scroll sideways to inspect the full table
| Red flag | Why it is fatal |
|---|---|
| A hand-written detail layer | Stale within a week at this scale. Detail is generated or it does not exist |
| A card that restates status | Wrong within a day, and it teaches people not to trust the map |
| A second methodology | ICM is already installed and already used. A parallel vocabulary doubles the cost and halves the adoption |
| Building the machine-readable projection first | An interface with no consumer is an unwired check wearing a different hat |
| Turning on all 803 checks | Unexplained failures teach people to ignore checks. Phase 4 exists to prevent this |
| Making flaky checks blocking | Same outcome, faster |
| The map living in a document nobody can regenerate | Then it is a snapshot, and snapshots rot |
| Editing a generated file | The next build discards it silently. This has already happened elsewhere in this estate |
Ventari Estate Map · Operating standard
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.
Scroll sideways to inspect the full table
| # | Question | Ratified answer (D20) | Owner after |
|---|---|---|---|
| 12.1 | Where the map lives, and who owns it | Written layer (regions, boundaries, blast radius) in VentariBrain/_canon/, beside the registry, so one folder stays where agents orient. Generated inventories in the app under docs/estate/_generated/, rebuilt by one npm run command |
Cody owns the generated layer; Nico's agents own the written layer, under Brain write rules |
| 12.2 | Findings register and risk acceptance | Alex records in docs/estate/FINDINGS.md in the app. Cody accepts or rejects each risk. Never the same person |
Alex records, Cody accepts |
| 12.3 | Does the Executive read the map | Yes, Phase 2 scope. A small server-owned read: the region index plus blast radius for the regions a task names, never the whole map in every prompt. Entry point is the prompt assembly in docs/executive-context.md. Superseded in part 2026-09-08 — see 12.8 |
Cody |
| 12.4 | Are the running checks reliable | Yes. Every push to main in the last 25 core-stability runs was green; the only failures were on feature branches mid-work. Cody turns on required status checks for VentariFullApp main. VentariBrain stays unprotected: the sync bridge and agents push records directly. The website is a later, separate call |
Cody |
| 12.5 | Repositories already known dead | No list exists. Phase 1 produces it. Anything unpushed for 60 days is leftover until a person confirms dead |
Phase 1 |
| 12.6 | Scope of the estate | Everything under joyblisscoder, plus the audit engine host and the two executive hosts. Client-owned repositories are out until a second installation exists (H7). Clarified 2026-09-08, see 12.9 |
Settled |
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.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."
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.
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).
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.
, 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.
, 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
Ventari Estate Map · Operating standard
Read on 2026-09-07 from committed code, generated documentation and repository settings. Nothing was modified.
VentariFullApp at origin/main, commit 1771e2f..github/workflows/core-stability.yml and verify-advisory.yml.scripts/core-stability-manifest.json: typecheck plus 15 checks.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.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.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
Two findings from this review are closed. Recorded here so the phases below are not planned against problems that no longer exist.
Scroll sideways to inspect the full table
| Finding | Landed | Evidence |
|---|---|---|
| The Brain could not be checked out on Windows | VentariBrain b0bee580 |
16 paths in three classes renamed. Fresh clone verified on Windows: 5,955 files, zero errors, git status clean |
| The folder-name bug could recur | VentariFullApp d54e8ffb |
safeVaultSegment now strips trailing dots and spaces and prefixes reserved device names. verify-vault-path-safety.ts is the 17th check and runs on every push |
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
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.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §2 and §3.4: a cross-repository map does exist | _canon/SYSTEMS-AND-FEATURES.md. This was 0.1's central premise and it was wrong. The map extends the registry |
| 2 | §3.3: 800 verify scripts, not 803, cited against 1771e2f |
0.1's count also matched live-verify-* and purge-verify-* |
| 3 | §9.3: the Executive read moves from P2 to Phase 2 | The consumer already exists; 0.1's reason to defer did not hold |
| 4 | §8: phases become program rows VE-178 to VE-183 | Five of six already had program identity. No parallel roadmap |
| 5 | §3.5 added: the governance layer | North Star, evolution program, claim protocol, D17 registry, Brain write rules, agent experience rules |
| 6 | §3.5: two binding decisions from the W0–W16 build | Rules in the database, one agent instance per paying client |
| 7 | §12: all six decisions ratified as D20 | Several answers differ from what 0.1 assumed, notably the split ownership in 12.1 |
| 8 | §12.7 added | Two things raised back rather than accepted silently |
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
Applied 2026-09-08. One correction, found while planning Phase 2 against this document rather than from memory.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §12.8 added: 12.3 superseded in part | Phase 2's Executive read returns the region index only. Blast radius is Phase 3 (VE-180) and joins the read there |
| 2 | §9.3 and §8.2: the "plus blast radius" clause removed from Phase 2 scope | Same correction, applied where the scope is actually stated |
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.
"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.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §12.9 added: H7 quoted from Nico's go brief as "second installation from configuration" | The document gated 12.6 on H7 twice without ever recording what H7 says |
| 2 | §12.9 and §9.3: "client-owned repositories" clarified to mean a client's own GitHub organisation | The original wording read as though client project repositories were excluded. They are not, and all 103 census entries prove it |
| 3 | §12.8 moved to sit after §12.7 | It was inserted in the wrong order in 0.4 |
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.
Both changes came out of building Phase 2 against this document, which is the only reliable way to find what a spec left ambiguous.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §6.3: status is defined as the card's freshness only, never a copy of a feature's implementation status |
§10.1 required status on every card while the go brief §3.1 forbade restating a registry fact called Status. A lexical collision, not two facts, but a card would have carried a second editable authority into the canon before anyone noticed |
| 2 | §6.3: entity becomes the stable region name, and members carries the census ids |
entity was defined as "the path or URL the card describes", singular. Most real regions aggregate a repository, a hosted project and a host, and five of the twelve have no row in the registry's ownership table at all |
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.
Phases 1, 2 and 3 have shipped. This version changes the phase that is next, before it is built, rather than after.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §8.4: deletion leaves Phase 4. The exit is a recorded verdict for all 824 plus the load-bearing ones running | The problem is that 34 checks run, not that 824 files exist. Deleting files makes nothing safer; wiring the right ones up does. Turning a check on is cheap and reversible; deleting one bites months later |
| 2 | §8.4 and §9.1: counts re-measured at app df62cfa |
824 scripts, not 803. 34 run, not 16. 41 security-named, not 47. And two figures the document never had: 665 reachable by an npm script, 159 orphaned with no npm script at all |
| 3 | §8.4: the registry's Verify: field named as the classification's first source |
It is on 36 of 39 features and names exact commands. Ventari's own claim about which checks matter, never once used |
| 4 | §8.4: mutation named as the acceptance test | Phase 3's lesson. A reviewer reading a plausible list finds it plausible; only evidence that can refute is worth having |
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.
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.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §8.5's exit becomes a measured verdict for A3, A4, A5 and H9 plus wiring the checks that already prove a gate | The original exit cannot be met while a gate is open. Keeping it would have forced either weakening a test until it passed, or leaving the phase open for weeks pending a fix nobody has approved |
| 2 | §8.5 records that the rollout is a separate row | Applying the predicate to 210 policies is a production schema change. Under 12.2 that is Cody's to accept, and a lane that finds a problem should not also ship its fix |
| 3 | §8.5 records that Phase 6 is blocked by A3 | A second installation would expose both tenants to each other's staff. The rollout is on the critical path to Phase 6, D14 and H7 |
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.
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.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §8.5: the "Phase 6 is blocked by A3" paragraph struck through and corrected | The chosen deployment tier is hybrid — peer tenants get their own database. A3 measures a shared-database boundary that tier explicitly rejected |
| 2 | §8.5: the three delivery models recorded | Rising Origin (hand-built, client's own org, shipped), growth-os/cvc-admin (forked, shipped), tenant-packs (the chosen path, not shipped) |
| 3 | §8.5: the real Phase 6 question named | 482 functions carry a 'ventari' literal. Whether that blocks a peer tenant depends on whether a private stack is internally 'ventari' or not. Nobody has answered it |
| 4 | §8.6: unblocked, with the two cheap experiments named |
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.
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.
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.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §8.6: Rising Origin named as the test case | Its 14 migrations rebuild identity, org membership, staff roles and security boundaries — all of which already exist in the core. The duplication is measured, not argued |
| 2 | §8.6 split into two stages, and only stage 1 is this phase | Stage 1 builds a Rising-Origin-shaped instance against a local database and records the gap list. Stage 2 is the real migration, its own row, gated on stage 1 and on the client agreeing |
| 3 | §8.6's exit rewritten to match stage 1 |
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 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.
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.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §8.6: stage 1 results block added, with all three planning questions answered | The section was written as a plan in future tense. It had answers and did not say them |
| 2 | §8.6: the 'ventari' literal count restated as 9 / 22 / 483, with the method beside each and the small number first |
482 is true and misleading. It counts functions that contain the string; only single digits to low twenties actually refuse a non-'ventari' tenant. That distinction is the difference between a config phase and a rewrite, and this document quoted the scary figure five times |
| 3 | §8.6: Rising Origin's migration list corrected from 7 names to the real 14 | Version 1.0 listed half of them and presented it as the list |
| 4 | §8.6: leads / lead_stage_history recorded as the core's opportunities / opportunity_stage_history |
Their own migration header says so. Version 1.0 listed them among the things the core might lack |
| 5 | §8.6: exit recorded as met, with the fixture caveat stated in the same breath | The check proves the detector on every run. ADR-005's published fixture is still open, and saying "exit met" without that would be the kind of half-truth this document exists to catch |
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.
A stage was missing between knowing the cost and proposing the move.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §8.6: stage 1.5, the dry migration, added between stages 1 and 2 | Stage 1 built a database, not an application. Nothing has been mapped, so "it would be a simple migration" is still an assumption |
| 2 | The exit measure is row counts in versus out, per table, every difference named | "It ran" and "no errors" are not evidence. A mapping that loses 3% of leads demos perfectly |
| 3 | The attribution contract made a named exit condition | Alex, 2026-09-09: if we build CRMs for clients, attribution should be one elegant thing that copies, not rebuilt per client. The core has the evidence layers and no adjudication ledger |
| 4 | Correction recorded against the stage 1 gap map's attribution reasoning | It weighed 0005 only. Acting on it would have meant rebuilding touches and the booking contracts, which already exist and are defended by two running checks |
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.
The dry migration ran, and it separated two answers this document had been giving as one.
Scroll sideways to inspect the full table
| # | Change | Why |
|---|---|---|
| 1 | §8.6: what stage 1.5 measured, with the counts and the four application-layer findings | The section recorded a plan. It had a result |
| 2 | §8.6: the stage 1 conclusion qualified. Configuration rather than a parameterisation project is true of the database | It was stated without that qualifier and repeated in a PR body, a lane brief and a conversation with the owner. The application layer is where the work is |
| 3 | §8.6: the fabrication class recorded beside refusals and corruptions | The first run cloned a human. Neither bucket the instruction gave fitted it, and a row that should not exist is its own failure mode |
| 4 | §8.7 added: Phase 7, make the application honour the pack | Four items, each with a file and a line. Nothing exploratory |
| 5 | The seed-data finding called out separately | A private stack ships with other clients' names in it. verify-install-fixture-cleanliness guards install fixtures; seed migrations are a different artifact and nothing checks them |
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
12 sections · Phases 9–14 · Factory v0.11
Part II · Phases 9–14
This is the installation, module, evidence and release standard for every CRM Factory instance.
Client CRM Factory · Build standard
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.main before any new design,
mobile, speed or functionality pass. Start at §4b and the dated checkpoint.runbooks/install-sequence.md) was reconciled with the CLI release standard
in #1073 and stays status: candidate and inactive until that proof passes.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
Merged to main on 2026-09-10 as PRs #549, #550 and #543.
Scroll sideways to inspect the full table
| Settled | Where |
|---|---|
| One codebase, one pack file per client, one database per client, one deployment per client | D20, and finding dc3c882 |
| A client instance renders as itself: pipeline, people, navigation, branding, tabs | Phases 7 and 8 |
| An unknown pack fails closed rather than inheriting Ventari's board | lib/crm/pack-pipeline.ts |
| No Ventari fact reaches a client surface, proved by a check that goes red | core-stability check 72 |
| Peer clients never share a database, so the A3 staff-permission finding is not a client blocker | 2026-09-10 call |
| Rising Origin's seven stages are the standard client pipeline | Alex, 2026-09-10 |
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.
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
This table is the original measured baseline. It is retained as evidence, not as today's installation checklist.
Scroll sideways to inspect the full table
| Step | State on 2026-09-10 |
|---|---|
| 1. Write the pack file | Nothing. Manual, and it is the easy part |
| 2. Register the pack | Nothing. A code change and a deploy |
| 3. Create their database project | Nothing. Manual |
| 4. Create their deployment | Nothing. Finding dc3c882 says it must be its own |
| 5. Set env, including host bindings | Nothing. Manual |
| 6. Point the domain | Nothing. Manual |
| 7. Build a clean database | The migration history could not build one at all until 2026-09-10. See below |
| 8. Keep it current as schema changes | Nothing. This is the one nobody has named |
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.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.
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.
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.
Scroll sideways to inspect the full table
| Step | Current factory path |
|---|---|
| 1. Define the client | Clone and edit a typed tenant pack; brand, navigation, modules, integrations and pipeline seed are declared together. This is still a reviewed code change. |
| 2. Register the pack | The pack registry and deploy/clients.json remain reviewed code. Unknown production hosts fail closed. |
| 3. Create the client repository | A private template-repository provisioner and approval-gated GitHub adapter exist. Their presence is not proof that a given client used them; Beeem's live repository was built directly. |
| 4. Create the database and deployment projects | Default remains a client-owned Supabase project, with the client inviting the installation operator. Proposed option, not the default: the Executive prepare/card flow is future work and is not implemented or enforced by this slice. A named admin can approve an exact Client Program plan with an expiring grant; the current first slice can create the approved project, but does not apply client-shaped migrations, claim public.instance_identity, prove database cleanliness, or issue the install receipt. Beeem's README records its Supabase project as created by Ventari. |
| 5. Configure environment and host identity | Environment values remain a human connection step. The deploy workflow validates the host binding, checks instance identity when readable, removes disallowed cron routes and fails closed on mismatches. |
| 6. Build the database | A clean core builds; agency-only migrations are explicitly skipped on client packs; instance identity can be claimed and verified. Applying hosted migrations remains an explicit operator action. |
| 7. Preview and release | Current standard: cross-platform Vercel CLI. Mac, Windows or Linux packages one exact merged source commit without Git metadata, applies the client pack's pruning, and uploads source for Vercel's Linux service to build as a preview. Root, binding, pack, migration and unbound-host behavior are verified before a separately approved CLI promotion. The integrated versioning coordinator is parked future infrastructure. |
| 8. Keep it current | With the baseline Supabase invitation in place, scripts/apply-migrations.mjs plans, applies and verifies by expected pack. Client releases currently use the documented operator-run CLI procedure after authorization. Migration fan-out is also deliberately run by an operator. The runner refuses files marked -- hosted: hold, out-of-order versions (without --allow-backfill), --only leaps (without --accept-skipped) and unknown versions, sends each migration and its history row as one payload, and refuses apply on the api transport until its transaction behavior is verified in a disposable environment. pending is not an authorized batch; see runbooks/migration-ledger-hygiene.md. |
| 9. Carry native intake capability | Every install carries the additive native-form, immutable-publication, adapter, durable-receipt and intake-evidence schema plus its renderer, routes, controls and verifiers. A clean install reports the capability installed but has no form, publication, origin, credential, mapping or active adapter. Configuration, publication and verification remain separate client-approved actions. |
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
Ordered by dependency, not by appetite. Phase 9 comes first because it is the only one that produces measurements rather than assumptions.
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.
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.
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.
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.
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.
<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.public, now that the names are free._legacy objects and the client's untouched tables
into the core's shape, using the mapping stage 1.5 already proved.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.
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.
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:
POST /auth/v1/admin/users with the service-role keyINSERT INTO peopleINSERT INTO profiles with id = the auth user id, linked to that personINSERT INTO role_assignments with role adminPUT app_metadata with ventariRole, ventariRoles, ventariOnboardedMiss 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.
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:
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./crm/clients, not
the client-360 vault at /clients, which is empty on a fresh instance.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.
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.
Scroll sideways to inspect the full table
| Group | Tokens | Status |
|---|---|---|
| Surfaces | background, surface, surfaceRaised, border, borderStrong |
background exists; four new |
| Text | foreground, muted, mutedForeground, mutedStrong |
three exist; mutedStrong new |
| Accent | primary, primaryForeground, primaryBorder, so an orange outline is expressible separately from an orange fill |
two exist; primaryBorder new |
| Status | signalGood, signalWarning, signalCaution, signalActNow, signalActive, signalNeutral |
all six exist, unchanged |
| Shape | radiusSm, radiusMd, focusRing |
new |
| Typography roles | display, label, body, each carrying family, weight, transform (none or uppercase) and tracking |
new; fonts.{body,display,mono} stay as the family source |
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.
, 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.
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.
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.
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.
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.
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.
, 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.
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.
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.
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.
, 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.
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.
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).
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.
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 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).
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.
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.
Scroll sideways to inspect the full table
| Permission | Socials / Connections surface |
|---|---|
pages_show_list |
List Pages the user admins; Page picker on Connections |
pages_read_engagement |
Facebook Page profile, posts, like/comment counts |
pages_read_user_content |
Facebook post content and Page insights the dashboard reads |
pages_manage_posts |
Publish / schedule Facebook posts from Socials |
pages_messaging |
Facebook Page inbox (Social pane) |
instagram_basic |
Instagram profile, media grid |
instagram_content_publish |
Publish / schedule Instagram image and reel |
instagram_manage_comments |
Instagram comments and reply |
instagram_manage_insights |
Instagram account and per-post insights |
instagram_manage_messages |
Instagram inbox through the Page-linked professional account |
Scroll sideways to inspect the full table
| Surface | Why it is separate |
|---|---|
| Threads | graph.threads.net. Own app product and OAuth (threads_basic, threads_content_publish, threads_manage_insights). A Facebook user/page token does not work. Later lane, same stored-first pattern. |
Instagram Login (graph.instagram.com + instagram_business_manage_messages) |
Separate token needed for Instagram DM triage. The instagram row from 9f contains the Page token, not this token. Configure INSTAGRAM_ACCESS_TOKEN per client deployment; the inbox checks /me against the connected Instagram identity. The current Connections card does not yet issue/store this token. |
| X | Own OAuth on Connections. Already a different card. |
| Google / Search Console | The Google card; one OAuth client per client instance under Ventari's Cloud account, named for the client (decided 2026-09-19). Loop 26, not this SOP. |
| Ventari's Meta app | Agency instance only. Never a credential on a client instance. |
(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.
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.
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.
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):
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.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.
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
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.@risingorigin → Rising Origin Page, confirmed.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)
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.){client product} CRM (Rising Origin: Rising Origin CRM), contact email the client's.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: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)
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.instagram_business_* scopes on
graph.instagram.com. Do not click "Add all required permissions"
there and do not note that ID/secret.instagram_basic, instagram_content_publish,
pages_read_engagement, pages_show_list, business_management) and
Add required messaging permissions (adds
instagram_manage_messages).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.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_*.{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.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)
me/permissions — all ten granted, nothing else but public_profile.pages_*
row names the client's Page id, every instagram_* row the Instagram
account id. That is the token shape Connect wants.{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.Then, on the instance (after the 9f release is on the client's host)
Not this configuration, not this card
"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.
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.
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.
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.
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.
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.
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.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.
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.
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:
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'.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.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).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.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).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.client-hosting-and-ai-standard-v1.md, pending Alex and Cody's sign-off.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:
lib/executive/client-crm-tools.ts):
pipeline_overview, find_contacts, list_tasks, upcoming_appointmentstoolSet: client_crm. Anthropic-direct has no tool loop
here, so the card recommends a gateway key and says the Anthropic key is
conversation only.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.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 toolsAdd 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.
Scroll sideways to inspect the full table
| Capability | Who initiates | Gate it needs that the others do not |
|---|---|---|
| Human triage and reply | Staff | Pack module social_dm_triage; staff auth. Live on Rising Origin and Ventari Agency. |
| Assisted drafting | Staff reviews every draft | AI provider and per-tenant data-use consent; draft never auto-sends. |
| Style learning | Staff-confirmed sends only | Per-client consent, retention and deletion policy; no personal Cody examples in any tenant. |
| Batch send | Staff approves each thread | Per-thread approval, throttle, window check, duplicate guard, delivery receipts. Batch approval of a plan is never approval of a message in it. |
| Timed follow-ups | Internal reminders first; outbound later | Internal needs-reply and waiting views need no consent. Any outbound cadence needs consent, opt-out, send-time rules and a working unsubscribe. |
| Automated flows | Provider event or trigger | native_flows module, client approval of the exact version, send grants, SOCIAL_SENDING_ENABLED, takeover and opt-out. |
Scroll sideways to inspect the full table
| Donor file | Parts | Class | Target and note |
|---|---|---|---|
static/draft_utils.js |
mergeBatch, captureEdits |
Direct port | Already on main as lib/comms/message-draft-batch.ts. T1 wires it. Port tests/draft_utils.test.js cases for parity if not yet covered. |
response_store.py |
effective_triage_status, snooze reopen on new inbound |
Direct port | T2. Pure rule over status and timestamps; re-express in TypeScript against social_dm_triage_states. |
response_store.py |
needs_reply_list, follow_up_waiting (5 days) |
Adapt | T2. Same selection logic; query the account-scoped table and live threads instead of local SQLite; threshold becomes configuration. |
response_store.py |
mass_candidates, save_analysis_edit |
Adapt | T6 and T1b only. Needs per-thread approval and the persistence decision. |
response_learning.py |
record_confirmed_response, retrieve_examples |
Adapt | T4. Confirmed-sent-only rule kept; needs tenant table, consent and retention. |
response_learning.py |
build_response_profile |
Do not use | Writes a personal profile file. |
outbox_reconcile.py |
history_shows_sent, reconcile_outbox_from_history |
Adapt | T3. Keep claim, verify, then send; use the existing outbox and effect guard; review the similarity threshold before use. |
server.py |
already_delivered, _mark_sent, _send_worker guard and throttle |
Adapt | T3 and T6. Delay becomes per-tenant configuration; fail closed on any unverified state. |
server.py |
HTTP routes, agent-handoff, refresh |
Do not use | Local server surface. |
meta_messaging.py |
send, live ingest, clone helper |
Do not use | main already has fetchSocialDmThreads and sendSocialDmReply. Take only the 23-hour Instagram window as configuration. |
suggest.py |
suggest_reply, digest_thread, DIGEST_SCHEMA |
Rewrite | T5. Keep the digest schema and prompt intent; call the Ventari provider port with consent, not grok_cli. |
ask.py |
ask, deep_history |
Rewrite | Deferred. |
db.py |
SCHEMA |
Rewrite | Postgres migrations per slice; never copy SQLite. |
static/app.js, index.html, style.css |
Views, filters, Mass cards | Rewrite | Extend SocialInboxPanel; reuse the view list as design. |
grok_cli.py, agent_handoff.py, brain_sync.py, brain_context.py, ventari_* modules |
Local agent and vault sync | Do not use | Personal tooling. |
refresh.py, imessage.py, screen_chat.py, wa_mentions.py, transcribe*.py, media_context.py |
Local device sources, voice, media | Do not use | Deferred capabilities; revisit only with a hosted design. |
analytics.py, training_dashboard.py |
Analytics | Do not use now | Later track. |
config.json, data/ |
Personal account values, profile | Do not use | Personal data. |
tests/ (test_response_store, test_response_learning, test_send_duplicate_guard, test_mass_sending, draft_utils.test.js) |
Test intent | Adapt | Port the cases with each matching slice. |
Codysmessages |
Markdown message archive | Do not use | Personal message content. |
Scroll sideways to inspect the full table
| Holistic donor | Ventari home | Port decision |
|---|---|---|
webhook-signature.ts, Instagram/Facebook webhook routes, webhook-dispatch.ts |
lib/socials/meta-webhook.ts, app/api/webhooks/meta/route.ts, lib/integrations/supabase-events.ts |
Keep Ventari's single callback and webhook_inbox; port only missing normalization, validation and dispatch behavior. Continue raw-body signature verification before JSON processing and reject account IDs outside the selected instance. |
rules.ts |
lib/automations/flows/meta-dispatch.ts, lib/automations/types.ts, components/automations/AutomationsConsole.tsx |
Reuse platform, account and post scoping. V1 supports exact, whole-phrase and contains matching after Unicode and case normalization; whole phrase is the default. Client-configured trigger values are preserved. Explicit priority determines precedence, and only the highest-priority matching flow runs for an event. |
send.ts |
lib/socials/automation-send.ts, lib/automations/flows/meta-worker.ts, lib/connections/store.ts |
Adapt the distinct comment-private-reply and direct-message paths, exact account pinning, provider-window checks plus required, verified send scopes (the donor fails open when scopes are omitted), and ambiguous-send handling. The worker invokes the sender from a stable outbox intent and records the provider receipt before completing the job. |
automation-send-reservation.ts |
automation_flow_runs, existing outbox_events, runtime_effect_guards, lib/foundation/runtime/durable.ts |
The persisted flow idempotency key reserves one run per provider event; the donor helper automation-send-reservation.ts (14 lines) only orders reserve-before-send; the unique-key reservation insert is in webhook-dispatch.ts; the existing outbox and effect guard reserve each send. Preserve ambiguous-send holds and never blindly resend. Do not create a second side-effect ledger. |
lesson-campaign-engine.ts, lesson-state-machine.ts, email capture rules |
lib/automations/flows/ (new typed step executor), existing CRM identity services |
Port as the first campaign fixture and reuse its validation, retry and handoff invariants. Express comment → private reply → inbound email capture/validation → link delivery as authored flow steps, not a permanent Rising-Origin-only engine. Keep email consent, identity collision and handoff as explicit policies. |
definition.ts, engine.ts, runtime/production/cron modules |
lib/automations/flows/types.ts, store.ts, meta-worker.ts, app/api/cron/automation-flows/route.ts |
The first worker uses a tenant-scoped durable cursor, leases, and outbox receipt. It currently supports one Meta reply followed by optional end only. Waits, branches, stop/takeover semantics, CRM linking and a client-visible run receipt remain to be built. Do not add a scheduler or worker per flow. |
Holistic social and workflows database schemas |
Ventari webhook_inbox, automation_flow_runs, existing social-channel and connection stores |
Do not copy donor tables wholesale. Add only missing execution fields and constraints to Ventari's run model; keep provider event dedupe, tenant isolation and per-send idempotency independently enforceable. |
hu-admin social automation and workflow consoles |
Existing components/automations/AutomationsConsole.tsx, Runs UI |
Port relevant validation, preview, activation, pause and run-inspection behaviors into the existing Automations surface. Do not create a parallel admin console or copy the AI authoring surface as a substitute for validated executable definitions. |
| Message Triage response profile, local thread memory and batch sender | No direct first-phase port; possible later reviewed-draft feature in the client inbox | Do not copy local stores, personal credentials, thread IDs, or personal memory. Consider response-style learning only after client-specific consent, tenant isolation, reviewed examples and retention policy are defined. |
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:
messaging-triage at 71fff2c1c6391d57c3c6b0e5e3b0adc2ecdfbd3f is Cody's source application. Codysmessages at 32d1a0941fb6ab3a7de28ef3be2945629d4c30b3 is a personal Markdown archive, not app code.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.
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.
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.config.json values, the response profile file, and every message in Codysmessages. These are privacy and tenant-isolation limits, not copyright limits.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.
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.
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.
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.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.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.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.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.
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.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.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.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.
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.-- 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.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.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.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.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.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.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.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):
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.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.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).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.
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.
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:
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.
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:
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.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.
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.
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.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).
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.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.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.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.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.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.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.
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.mdcrm-factory-native-chatbot-2026-09-30/reports/02-crm-native-implementation-map.mdcrm-factory-native-chatbot-2026-09-30/reports/01-source-access-and-capability-audit.mdcrm-native-automation-build-2026-10-01/reports/01-donor-evidence.mdcrm-native-automation-build-2026-10-01/reports/02-ventari-runtime.mdcrm-native-automation-build-2026-10-01/reports/03-rising-origin-journey.mdcrm-native-automation-build-2026-10-01/reports/04-instance-operations.mdcrm-native-automation-build-2026-10-01/reports/05-interface-freeze-proposal.mdThe 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.
verify:client-surface-leak:unbound already asserts it and exits 1.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.
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.
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.
How a schema change reaches every client. Code propagates on deploy. Schema does not.
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.
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.
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.
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.
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.
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.
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.beeem_paid_client
was rewritten on this rule before its first hosted run.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.
Only now is there something worth automating, because Phases 9 to 12 have made each step real and measured.
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 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.
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.
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:
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.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.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.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.
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.
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.
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.
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.
.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.
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.
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 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.
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.
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.
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.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.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.
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.
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.
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.
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.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.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.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.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.
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.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.lib/booking/routes-for-instance.ts) replaces fourteen private copiesbooking_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.
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.
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:
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.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.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.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.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.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.
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.
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.
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.
The skill is the runbook, once the runbook has been executed twice by people and has stopped changing.
skills contract
(SkillBinding { id, enabled, requiresCapabilities }).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:
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.
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
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.
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
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.
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.
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.
Scroll sideways to inspect the full table
| # | Workstream | Status | Next action / completion evidence |
|---|---|---|---|
| 1 | Shared native-intake capability | DONE. PR #1021 is merged; migrations were applied to Ventari and Rising Origin; Rising Origin source 788cd1bd was manually deployed by CLI. |
This proves the shared capability is installed, not that a real client request has arrived. |
| 2 | Forms tenant visibility | MERGED, NOT RELEASED TO RISING ORIGIN. PR #1071 (38c93a6d) is on main. |
Make a separate CLI client release, then log in and verify the agency and Rising Origin Forms views. |
| 3 | ChatbotBuilder connection | CONFIGURED, NOT PROVEN. The adapter row is connected in preview/bearer-header mode. Alex's app Test is synthetic: it checks bearer authentication and envelope handling, compares repeatable digests, and checks handoff mapping when applicable. 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). The Sept. 28 snapshot showed no received timestamp or intake receipt. |
After authorized access and tenant/database verification, run a controlled real request and replay. Completion evidence: correct tenant, receipt, received timestamp, and replay result. |
| 4 | Email delivery | NOT PROVEN. ChatbotBuilder's Resend SMTP nurture and CRM-sent email are separate paths. | Confirm the intended Rising Origin sender/account; never use Ventari credentials. Each path needs its own delivery receipt. |
| 5 | Public project-request cutover | BLOCKED. Publication 2 had no allowed origins, and the submit route does not enforce that list. GoHighLevel remains the live route and rollback. | Fix and prove the origin boundary or use a reviewed same-origin proxy; publish the correct origins; prove the end-to-end path and rollback before a separately approved cutover. |
| 6 | Booking cutover | SEPARATE, NOT CUT OVER. Booking remains on GoHighLevel. | Prove booking independently; do not bundle it with the form cutover. |
| 7 | Packet A synthetic proof | PARKED. It writes synthetic business records into the production CRM database and is not proof of a real provider request. | Do not resume without a fresh baseline and separate explicit approval. |
| 8 | CRM Factory Phase 14 acceptance | OPEN. A second canonical one-codebase client install and an inspectable run receipt are still required. Beeem does not satisfy this acceptance test. | Complete the second canonical install and retain an inspectable receipt for the ordered install sequence. |
Two separate finish lines
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.
Scroll sideways to inspect the full table
| # | Precondition | State | Evidence |
|---|---|---|---|
| 1 | Second install, run by someone who did not write the sequence | Not met | deploy/clients.json lists Rising Origin (the first canonical install), Cymatica (outside this rollout, §5b) and Myth Ethos (kind: standalone). Beeem is excluded: its source tree is separate and it is not in deploy/clients.json (§1). An isolated synthetic installation can satisfy this gate on the conditions in Next Phase 14 step; a prototype or tabletop mock alone does not. |
| 2 | The sequence did not change during that install | Not met | runbooks/install-sequence.md is status: candidate. It was reconciled with the CLI release standard in #1073 and is still moving: 13e, 13f, analytics activation, module pruning. |
| 3 | Every step has a check that can fail | Partial | verify:crm-factory-sequence requires a check, a pinned gate and a receipt field on all 17 steps. Steps 1, 3, 4, 13 and 14 are human proofs with a named person. |
| 4 | A human has walked the resulting instance | Not met | Needs gate 1. Ledger step 14: desktop and 390 px, a named walker, dated. |
| 5 | Phase 9c token contract, with shell, funnel and CRM converted | Met | Contract decided 2026-09-11. Shell #578, funnel #583 and CRM #577 were converted, as were team #576, agents #580 and auth #585. The gate ran 2026-09-13. verify:token-contract runs in core-stability. |
| — | One inspectable run receipt (2026-09-24 checkpoint) | Not met | No receipt exists. verify:crm-factory-receipt (receipt v2) requires the released commit merged into origin/main, core-stability passed at that commit, every step pass, and a closed receipt. |
| — | Where a skill body lives (Decision 2) | Decided | Decided 2026-09-19 (§5, item 2). The client half, bundling a pack's bound skills into a client deploy, is 13f: approved, not built. |
This table records evidence and grants no execution authority; every ledger step still waits for the person named in its Gate column.
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:
runbooks/install-sequence.md;verify:crm-factory-receipt;A synthetic installation also needs two things:
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.
Scroll sideways to inspect the full table
| Order | Candidate | Direction and boundary |
|---|---|---|
| 1 | Configuration-first modularity | Prefer supported per-client configuration for module selection, navigation and layout choices, labels, fields, and workflow settings. Keep client data and configuration separate between instances. A request that cannot be expressed within the supported configuration contract becomes an extension candidate, not a client-specific edit to the shared core. |
| 2 | Client-owned extensions and SDK | Future target: Ventari keeps the shared product core private. If prioritized, a versioned SDK/API would expose documented, permission-bounded extension points; customer extension source would live in a customer-owned repository. Extensions and their authors would have no read or write access to Ventari's core. Define the security, compatibility, review, preview, and activation contract before implementation. |
| 3 | Automated Intake as a configurable module | Intake capability and its delivery workflow remain documented in §13e and the Automated Intake specification; this roadmap does not duplicate that work. After Phase 14, evaluate how an instance selects, configures, and exposes the capability through its pack/modules. Offering it as an optional package or upsell is a candidate commercial decision, not a settled price or current release claim. |
| 4 | School module | Candidate for a clean-room, reusable education module in the same Ventari-maintained product codebase, deployed to separate client instances with separate data, configuration, and extensions. Explore courses, lessons, enrolment, progress, and access, with a shared person identity and relevant school activity visible to the CRM. Use existing school products only as requirements and architecture references; do not copy, port, adapt, or reuse their source code in this clean-room module. |
| 5 | Additional modules from proven need | Add future capabilities only when a concrete client or product need is recorded with evidence, intended outcome, owner, dependencies, and an explicit prioritization decision. Discovery alone does not start a build lane. |
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.
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.
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.
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.
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.
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:
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.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.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).
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.
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/:
Scroll sideways to inspect the full table
| File | What it is |
|---|---|
SPEC.md (this file) |
Source of truth. Edit here, same PR as the code. §4b, §13e, §5 |
ghl-exit.md |
GHL-exit build brief: field keys, site widgets, worker briefs |
runbooks/cutover.md |
In-place cutover for a client who already has a system |
BUILDING-ON-THE-CRM.md |
Rules before any UI / tenancy edit |
ESTATE-MAP-SPEC.md |
Phases 1 to 8 (code). D20 |
README.md |
This folder's map |
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.
Scroll sideways to inspect the full table
| Phase | State | What closes it |
|---|---|---|
| 9 Migrate Nick | Done. Rising Origin runs the shared codebase on his pack, at his URL, on his own Supabase and Vercel projects, with his logins and data. Live release client/rising-origin/v24 (2026-09-15). Host rising-crm.vercel.app is trusted_host |
Nothing |
| 9b Cutover for a client with a system | Done, scripted through the Supabase Management API (no database password), rehearsed four times, run once for real. Twenty minutes | A second client runs it |
| 9c Brand fidelity | Done. Contract, extractor, six conversions, the gate, design roles; his funnel and CRM wear his look | Alex's second look on the live instance, whenever |
| 9d Surfaces ask the pack | Done. Identifiers from the pack, secrets from the environment, nothing falls back to Ventari | His real connections, which he makes himself (below) |
| Connections (9d, revised) | Live on v28 (2026-09-20): mailbox, sending, Google, X, GHL, Meta. GHL re-entered after the key rotation (read-only token). Meta connected on his own app. Google, three surfaces on a client instance, one OAuth client (Rising Origin CRM, project rising-origin-crm under Ventari's Cloud, External, published unverified): (1) the Connections card - calendar + Search Console grant, stored per user; (2) the Growth Search page - reads that grant but only once searchConsole.propertyUrl is on the pack (null today; v29); (3) CRM sign-in with Google - Supabase Auth's Google provider on his project, enabled 2026-09-20 with the same client plus the Supabase callback as a second redirect URI, and rising-crm.vercel.app/** in Supabase's redirect list. Google's consent screen names the Supabase host, not the app, until brand verification; the plan is a Supabase custom domain (auth.risingorigin.com), not verification, because verification wants Ventari's account as an owner of his domain. Resend: his own key, not entered. No AI card |
Nick (Tuesday): Search Console property (domain or URL-prefix), Google connect as info@, Resend key + DNS, X (optional), mailbox (Google Workspace app password), the custom-domain CNAME. Then v29: property URL on the pack, NEXT_PUBLIC_SUPABASE_URL on the custom domain, and the Connections how-to copy (#839) |
| 9e Design roles | Landed (#616); then Nico took five of the seven gate decisions for Ventari's own instance (#630), shipped as declared values with his written yes on #624, the first change to the agency's look, by the mechanism | Nothing |
| 9f Meta connections | Live on Rising Origin, 2026-09-20 (v28). His own Meta app (Rising Origin CRM, business-owned, created from his portfolio by the use-case path; config 1082330371075711), entered on Connections, connected as Nico; Facebook Page 1284116174786165 and Instagram 17841477864741917 stored encrypted in his instance; Socials reads both from the stored tokens; his Vercel project carries no Meta variable (env checklist 2/17, both X). Three fixes on the way in: Pages listed from debug_token granular scopes when /me/accounts is [] for a portfolio Page (#833), config_id over scope (#833), Instagram over graph.facebook.com for a stored Page-linked token (#836, after the first live read failed with #100 / #3 on v27). SOP rewritten from the walk. Model B unchanged: no Ventari-held Meta credential on a client instance. Alex holds Meta developer access on the Ventari Business; Ventari's own app is for the agency instance only |
Nick posts to his Page and Instagram from Create / schedule — his content, his call. |
| Sidebar (polish PR 1, #875) | Decided 2026-09-20 (Alex): the agency's rail is the rail. Grouping applies on every pack - Today / Relationships / Growth / Operations / Workspace - and a group is a property of the surface (/pipeline is Relationships wherever it appears), so no pack field and no client-specific names; a group without a tab does not render. The staff "Workspace" switcher is gone on every instance; Client Health is a link only where the pack declares a client-health pipeline. The Ask Executive launcher hides while any drawer is open (it sat on the Funnel drawer's Save button). Rising Origin lands as Relationships (Funnel, CRM, Clients, Contacts, Messages), Growth (SEO & GEO, Social Media), Workspace (Team, Agents, Chat) |
Rides v30 if merged by Tuesday, else v31 |
| 9g Parity with the hand-built CRM | Done, live on v12. 9g-3 / 9g-2 / 9g-1 as before. Corrected 2026-09-15: CRM had no New company, and a contact with no company could not be opened — leftover of treating CRM as org-only. New company on /crm/clients. Live on v23: every Contacts row edits name, email, and phone on the person (person_emails / person_phones); company is optional |
Audits & Proposals as a tab or notes: Nick's call |
| 10, 11, 12 (client two) | 10 unbound hosts fail closed. 12's script is live. Both hosted databases claimed (Ventari first, then Rising Origin); fan-out as of 2026-09-15: pending 0 on both (Ventari 331 applied / core 324; Rising Origin 324/324) | Bind every host on each Vercel project, including preview *.vercel.app; then a second client |
| 13a Deploy action | Working; hardened 2026-09-20. v25-v29 shipped and smoked green; the dry run works again (#834), no git metadata reaches the client's project (#835), the smoke asserts the migration version (#841). Procedure in §13a | GitHub Pro for the approval button |
| 13c Pruning | Cron slice done: a client's build carries only the cron routes its pack allowlists. Rising Origin ships three daily routes: standard GSC snapshot (06:20 UTC), standard measurement snapshot (07:20 UTC), and automation-rules (08:40 UTC) | Continue module pruning; check each allowlisted schedule against that client's Vercel plan |
| 13d GHL mirror | Landed as a client connection: card, calendars, inbound mirror. First catch-up 2026-09-15: 14 people, 5 appointments, 0 opportunities (he has no GHL pipeline), 0 organisations. Agency unchanged | Webhook URL in his GHL only if he wants live updates before the exit. Conversations not mirrored (13e / 13d correction) |
| 13e GHL exit | Form intake live on main and proved on the agency instance 2026-09-20 (Phase 13e.1; #868, #872, #873), not yet on his: five posts against my.ventari.media behaved exactly as specified (201 deal, 200 duplicate, 403 foreign origin, 200 form-encoded, 202 honeypot) and the deal landed at the first stage of ventari_sales with the contact attached. The proof found two live misses the verifiers had run around - the route behind the session middleware, and the Connections page dropping the card on load - both fixed the same hour. #876 closed and proved live on Ventari 2026-09-20 (#878): a disposable PostgreSQL run replayed 375 ordered core migrations and passed the lifecycle verifier; migration 20260924200000 was then applied to the ventari-agency production instance and recorded exactly once. A live unassigned, forecast-empty intake deal moved forward with reason, next action, and due date, then moved to Lost with a closing reason only. Closing writes probability 0 and clears the follow-up; forward moves default probability from the target stage, forecast from the deal amount when present, and currency from the deal. Migration 0149's money-stage amount rule and board prompt remain. POST /api/intake/<packId>/<formKey>, the Website form card (Connect / Disconnect, the address, a counter - nothing else), pack-driven, public mode for a static site (origin allowlist + per-IP and per-endpoint rate limits + honeypot + duplicate window; Turnstile named as the upgrade if junk ever matters) with an HMAC signature for senders that can sign. Automations built 2026-09-21 (13e.2): the two GHL rules as templates on his pipeline, send_email through his own Resend key, agency never sends, stage-entered rules fire inline from intake; timed follow-ups use the daily automation-rules cron; seeded and activated with him once his key is on the card. Booking as a pack surface built 2026-09-21 (13e.3): one instance-aware route loader, no agency defaults or Helix on a client, pack tokens and copy, shared-busy agency-only, host = the Google his user connected; seeded per instance. All three replacements are on main; the lane closes when they are live on his instance and he cancels GHL. Site URLs in §13e |
v30: Connect the Website form card on his instance; his Resend key on the Sending card; seed + activate the two rules (seed-client-intake-rules --pack rising-origin --apply --activate); point /contact at the endpoint (his site repo, explicit ask); one real submission becomes a deal and two emails; retire the GHL form and its two workflows. Booking: Nick connects Google on Connections; seed intro-call with his email and --origin https://rising-crm.vercel.app; his site's Book-a-Call → /book/intro-call; one real booking on his calendar and in his CRM; retire the GHL widget; he cancels GHL and 13e closes. Then the AI card (9h, key-paste) |
| Ask Executive | 9h.1 key-paste built 2026-09-21, on main after this PR, not yet released or live-proved. The Assistant card on Connections: Anthropic key or AI Gateway key (any major model), verified with the provider before it is stored encrypted; a client instance answers a turn on that key alone through modelAccessForTenant (seat-stripped env, one lane laid over); disconnected → 403; picker hidden on a client; launcher mounts when the card is connected. Boot backstop and the sync gate unchanged; agency unchanged. Rising Origin declares the card. Detail in Phase 9h |
v30 or v31: Nick pastes his Anthropic key (or a gateway key + model) on the Assistant card; one turn answers; disconnect → 403. 9h.2 host-connect: parked 2026-09-21 (Alex) - bespoke, on request, quoted; never a standard offer, no installer planned. Mode 3 (Ventari-managed, metered) waits on a price |
| Client two - beeem | Live, but standalone. lib/tenant-packs/beeem.ts remains the shared-core pack declaration. joyblisscoder/beeem-crm is now a separate application with staff login, a seven-stage board, contacts, origin-restricted capture, Shopify webhooks/newsletter ingestion, a dedicated Supabase project and a production Vercel deployment. It does not run the VentariFullApp client build, is not in deploy/clients.json, and has no client/beeem/v<n> release. It is a client system, but not Phase 14's second canonical factory install. |
Decide whether Beeem remains an approved standalone Shopify CRM or is migrated onto the shared pack runtime. Reusable Shopify ingestion belongs in a selectable core module either way. |
| 13f Skills on a client instance | Scoped 2026-09-19 (§5, item 2). A client deploy carries no vault today, so a pack's skill binding resolves to nothing. The release bundles the bound skills read-only; the agency instance serves licensed skills to client instances over an endpoint; the vault is never mounted or tokened on a client host | A lane, after 9f and 13e |
| 14 The skill | Not started. Must not start. Preconditions 1, 2 and 4 are not met, 3 is partial, and no run receipt exists; see §4b Phase 14 gates. There is no second independent install on the canonical shared-core path, and the sequence is still changing (13e, 13f, analytics activation, module pruning). The ordered sequence is runbooks/install-sequence.md, reconciled with the CLI release standard in #1073 and still status: candidate. Its definition exists (§5): a skill is a Playbooks entry with canonical frontmatter; an install bundles what the pack binds. Beeem's standalone speed supplies useful inputs but does not freeze this sequence. |
Second canonical install by someone who did not write the runbook; then freeze the sequence |
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.
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.
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.
What the team should know, plainly.
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.Concepts/operations/crm-factory.md)
is owed; do not wait on it. This folder is the pickup.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).useTenantChrome().productName (AccountSettings.tsx). Agency
still reads "Ventari"; his instance reads his product name.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.
/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./crm/clients.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
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.
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.
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
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:
Scroll sideways to inspect the full table
| State | Current items |
|---|---|
| Settled factory rules | Phase 9's observed migration was accepted; unknown hosts fail closed; each client owns its instance, data and provider accounts; the token contract governs client branding; Contacts is a pack-declared core surface; form intake, client-configured automations and native booking are standard capabilities; one Google OAuth client belongs to each client instance. |
| Approved, implementation or rollout pending | Client skill bundling/licensing (13f); Rising Origin Automations enablement; production proof for intake, booking, first-party tracking and any provider connection not yet returning evidence. |
| Client choices | Optional Client Success; Audits & Proposals beyond notes; whether a client buys bespoke host-connect; which optional connections and modules it activates. |
| Open commercial decision | Price and policy for Ventari-managed model usage. Client-key AI is the standard built mode; host-connect is parked bespoke work, not a second standard setup path. |
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.
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.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.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).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.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.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.Client CRM Factory · Current operations
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.
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.tenant_id to client users as
"ventari (fixed)". The value is correct; showing it is not.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.
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.
A client cannot unlock modules on their own through anything we ship: the
pack (lib/tenant-packs/<packId>.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.
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.
Scroll sideways to inspect the full table
| Label | When |
|---|---|
| Not connected | No saved token |
| Checked | The token can see the project, and Vercel did not say whether Web Analytics is on |
| Analytics off | Verification recorded Web Analytics as off. The presence count is not called |
| No visits yet | Web Analytics is on and the last 30 Pacific days counted zero visits and zero page views |
| Unavailable | That count failed or was unreadable. The token stays saved |
| Reading | That count returned a visit or a page view |
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.
(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.
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.
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.
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 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.
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.
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.
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:
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.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.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.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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
Scroll sideways to inspect the full table
| Wave | State | Scope and exit |
|---|---|---|
| Gate 0 · X ownership clearance | VERIFIED / CLEARED | Final clearance releases Social and Connections presentation files. X logic remains excluded and unresolved; no source commit was produced. |
| Wave A · source truth and collision preflight | BLOCKED | Re-run against fresh origin/main after pull requests 969, 974 and 975 are merged, closed or revised. Create no claim, worktree or worker before the overlap check is clean. |
| Wave B · design and information hierarchy | HELD | One lead block and one useful chart or strip on each first screen; the rest follows below. No new data sources. Do not touch Autopilot or another owner's Money source truth. |
| Wave C · responsive/mobile | HELD | Check each surface at 390px. Fix wrapping, overflow, card hierarchy, drawers and chart readability per surface; do not use one global CSS guess. |
| Wave D · speed and efficiency | HELD | Measure before changing. Render independent evidence first; defer slower provider reads. Missing evidence remains unavailable, never zero. |
| Functionality pass | PLANNED · SEPARATE LANE | Walk visible UI through authorization, command, persistence, audit and refresh. Product delete means archive/remove with Undo, restore and Show archived; no hard-delete console. |
| Client intake and delivery architecture | ACTIVE · CLIENT PORTAL | One branded authenticated post-sale Client Portal carries intake, onboarding and automated client communications, and routes facts into their canonical homes. It is not the CRM, a second CRM or a form engine. The Rising Origin golden run goes through the portal (Automated Intake delivery map, Phase 6). Promote the workflow into the factory skill only after that golden run. |
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.
24h, 7d, 30d, 90d, All, Custom
and reuse one existing picker.13.4k) while
an accessible title or detail exposes the exact value.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.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.
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.
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.
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.
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.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.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.
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:
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).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 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 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
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.
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.
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.
Scroll sideways to inspect the full table
| One codebase, packs, pinned releases (as built) | One codebase plus an instance repository per client | A separate codebase per client | |
|---|---|---|---|
| What is the client's | pack, database, hosting, secrets | the same, plus the release decision and the pin, in a repository they can own | a copy of everything, including the bugs |
| A security fix for all clients | one fix, one review, then one approved release per client | the same; the bump is a pull request in each instance repository | N fixes by hand into N drifted copies, or the fix never reaches the ones nobody remembers |
| A change breaks something | Ventari's own instance first; a client only when a person approves a release for them | the same, and the approver can be the client | only clients who pull it, which they must to get any fix; and a copy nobody pulls to is a copy nobody patches |
| A change to a module a client does not have | today: gated, and the gate is proved on every pull request, but the code still ships. With Phase 13c pruning: not in their deployment at all | the same | not applicable, and not testable either |
| A client wants their own code change | leaves the factory, bespoke build | the same | trivial, and the moment it happens the copy can never take a shared fix cleanly again |
| Checks | 77, on every pull request, on the one codebase | the same | none, unless copied and then maintained N times |
| Compromised Ventari GitHub account | cannot ship to a client past the environment's required reviewer; cannot read environment-scoped client secrets from a branch | the same, and if the client owns the instance repository, a different account holds the release decision | the same account owns all N repositories; nothing gained |
| Cost to add client twenty | a pack file and an environment | plus one small repository | a twentieth copy of the codebase to keep alive forever |
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).instance_identity).
Pointing a pack at another instance's URL is refused at startup, deploy,
and fan-out once the row exists.verify:tenant-aware-shell and the leak check; and a client
never has to take a release they did not ask for.main, and the one-codebase model
is the only one where the fix ships to everyone the same day.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
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.
app/(dashboard)/<x>, app/api/<x>,
components/<x>). 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.components/ui) and which is a module, and that audit
is worth having regardless.client/*.main.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.
Scroll sideways to inspect the full table
| He wants | Where he gets it |
|---|---|
| To see what is deployed, and the logs | The client's Vercel dashboard: deployments, logs, domains, same as today |
| To see the build and what triggered it | The Actions run in VentariFullApp, where he has GitHub access |
| To control whether a client updates | The required-reviewer approval on the client's environment; nothing ships to a client past him |
| Previews per pull request | Ventari's own Vercel project keeps the Git integration on VentariFullApp |
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:
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.
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.
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.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.
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
Recorded because the table in section 2 is already slightly out of date, which is the point of section 7.
Scroll sideways to inspect the full table
| Merged | What |
|---|---|
9eb4cab #549 |
Phase 7, the application asks the pack |
1e6f177 #550 |
Phase 8, the pack owns the chrome, plus check 72 |
dc3c882 #543 |
one deployment per client, re-measured to 51 cron entries |
cb679cc #551 |
brand fields, Rising Origin's real colours, the agent tab |
| #553 | core stability can launch a check on Windows |
| #557 | a fresh database can be built at all, and brand assets live behind the pack |
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
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.