fullstackgtm 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +193 -0
- package/README.md +18 -7
- package/dist/cli.js +634 -56
- package/dist/connectors/outboxChannel.d.ts +64 -0
- package/dist/connectors/outboxChannel.js +170 -0
- package/dist/connectors/prospectSources.d.ts +22 -0
- package/dist/connectors/prospectSources.js +43 -0
- package/dist/connectors/signalSources.d.ts +105 -0
- package/dist/connectors/signalSources.js +316 -0
- package/dist/connectors/theirstack.d.ts +84 -0
- package/dist/connectors/theirstack.js +125 -0
- package/dist/icp.d.ts +47 -3
- package/dist/icp.js +105 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +3 -0
- package/dist/judgeEval.d.ts +7 -0
- package/dist/judgeEval.js +8 -1
- package/dist/runReport.d.ts +26 -0
- package/dist/runReport.js +15 -0
- package/dist/schedule.js +18 -4
- package/dist/signals.d.ts +54 -0
- package/dist/signals.js +64 -0
- package/dist/tam.d.ts +225 -0
- package/dist/tam.js +470 -0
- package/docs/api.md +18 -4
- package/docs/outbox-format.md +92 -0
- package/docs/recipes.md +37 -0
- package/docs/roadmap-to-1.0.md +31 -1
- package/docs/signal-spool-format.md +162 -0
- package/docs/tam.md +195 -0
- package/llms.txt +68 -5
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +3 -2
- package/src/cli.ts +714 -51
- package/src/connectors/outboxChannel.ts +202 -0
- package/src/connectors/prospectSources.ts +57 -0
- package/src/connectors/signalSources.ts +363 -0
- package/src/connectors/theirstack.ts +170 -0
- package/src/icp.ts +113 -11
- package/src/index.ts +32 -0
- package/src/init.ts +3 -0
- package/src/judgeEval.ts +8 -1
- package/src/runReport.ts +39 -0
- package/src/schedule.ts +20 -4
- package/src/signals.ts +90 -0
- package/src/tam.ts +654 -0
package/docs/roadmap-to-1.0.md
CHANGED
|
@@ -140,8 +140,38 @@ outward, one layer per release, each consolidating before the next expanded:
|
|
|
140
140
|
broker-https enforcement, the build/CI dist-integrity gate (published dist is
|
|
141
141
|
provably from source), Salesforce-merge capability matrix.
|
|
142
142
|
|
|
143
|
+
## 0.29 → 0.43 — from hygiene to the full outbound loop, as shipped
|
|
144
|
+
|
|
145
|
+
The next train extended the governed spine from *fixing the CRM* to *filling
|
|
146
|
+
it and acting on it*, without ever loosening the write gate:
|
|
147
|
+
|
|
148
|
+
- **0.29–0.34** — market-map maturation (config suggestion + taxonomy types,
|
|
149
|
+
sourcing helpers, competitor discovery, vendor logos/scale in the report)
|
|
150
|
+
plus call-type classification with type-specific coaching rubrics (0.32);
|
|
151
|
+
Salesforce `dedupe`/`merge_records` via SOAP and pipeline-aware closed/won
|
|
152
|
+
detection (0.30).
|
|
153
|
+
- **0.35–0.36** — DX cohesion: progressive-disclosure help, summary-first
|
|
154
|
+
`audit` with next-step guidance; the engagement workspace — a deterministic
|
|
155
|
+
`health` verb over a per-profile timeline.
|
|
156
|
+
- **0.37–0.39** — acquire: `enrich acquire` (net-new, ICP-targeted,
|
|
157
|
+
resolve-first deduped, metered lead creation), lead assignment policies so
|
|
158
|
+
leads are never born ownerless, the LinkedIn (HeyReach) discovery source
|
|
159
|
+
with email resolution, and run observability for broker-paired CLIs.
|
|
160
|
+
- **0.40** — Salesforce reaches full write parity with HubSpot
|
|
161
|
+
(`create_record`); HubSpot date-field writes no longer false-conflict.
|
|
162
|
+
- **0.41** — signal-based outbound: `signals fetch/list/outcome/weights`,
|
|
163
|
+
`icp judge`/`icp eval` (timing × fit × memory, golden-set calibration
|
|
164
|
+
gate), `draft` (trigger-grounded openers as governed plans), and the LLM
|
|
165
|
+
base-URL override.
|
|
166
|
+
- **0.42** — bulk apply for lead creation (HubSpot batch + Salesforce
|
|
167
|
+
composite).
|
|
168
|
+
- **0.43** — the on-ramp and the seams: `init` workspace scaffolding,
|
|
169
|
+
`docs/recipes.md` (five composable plays), and the contact/account seam
|
|
170
|
+
campaign — contact-granular outbound end to end, account-level acquire for
|
|
171
|
+
ABM, `health` by object type.
|
|
172
|
+
|
|
143
173
|
The known-gaps list below predates these layers and has been re-verified
|
|
144
|
-
against the 0.
|
|
174
|
+
against the 0.43 surface (2026-07): still accurate, still open.
|
|
145
175
|
|
|
146
176
|
## Known real-portal gaps to close before 1.0
|
|
147
177
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Signal spool format (webhook landing zone)
|
|
2
|
+
|
|
3
|
+
The **spool** is how push-based signal platforms — website de-anonymization
|
|
4
|
+
(RB2B), social/job listening (Trigify), form-submission webhooks (HubSpot) —
|
|
5
|
+
reach `fullstackgtm`. The CLI is a deterministic batch tool, not an always-on
|
|
6
|
+
server, so it does not receive webhooks itself. Instead:
|
|
7
|
+
|
|
8
|
+
1. A **receiver** you run (any HTTP endpoint — 20 lines, or an off-the-shelf
|
|
9
|
+
webhook-to-file tool) appends one JSON row per event to a spool file.
|
|
10
|
+
2. `fullstackgtm signals fetch --connector file` reads the spool on its next run,
|
|
11
|
+
turning each row into a ranked signal through the same evidence-gate → dedup →
|
|
12
|
+
weight pipeline as every other source.
|
|
13
|
+
|
|
14
|
+
The spool format and the `file` connector that reads it are open source. The
|
|
15
|
+
package ships **no receiver** — operating an always-on endpoint is out of scope
|
|
16
|
+
for the CLI (a managed receiver is part of the hosted product). This doc is the
|
|
17
|
+
contract your receiver writes to.
|
|
18
|
+
|
|
19
|
+
## The format
|
|
20
|
+
|
|
21
|
+
A spool file is **newline-delimited JSON (JSONL)**: one JSON object per line,
|
|
22
|
+
append-only. (A single `.json` file containing a JSON array is also accepted, for
|
|
23
|
+
convenience when staging by hand.) Each object is a **staged signal row**:
|
|
24
|
+
|
|
25
|
+
| Field | Required | Meaning |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `bucket` | yes¹ | One of `demand`, `funding`, `job`, `company`, `social`. |
|
|
28
|
+
| `accountDomain` | yes | The company domain. `domain` is accepted as an alias. Normalized (protocol/`www`/path stripped, lowercased) on read. |
|
|
29
|
+
| `trigger` | yes | Short human label, e.g. `"visited pricing page"`, `"raised Series B"`. |
|
|
30
|
+
| `quote` | yes | **Verbatim evidence** — the exact text a human can verify (a headline, a page title, the submitted form + email). A row with no quote is **dropped**, never faked. |
|
|
31
|
+
| `sourceUrl` | no | Link to the evidence. |
|
|
32
|
+
| `firstSeen` | no | ISO 8601 timestamp. Defaults to the fetch time. |
|
|
33
|
+
| `weight` | no | Explicit weight; defaults to the bucket's configured weight. |
|
|
34
|
+
|
|
35
|
+
¹ `bucket` may be omitted in a spool file — the `file` connector fills its
|
|
36
|
+
default (`company`). Set it explicitly per row when a source spans buckets.
|
|
37
|
+
|
|
38
|
+
Example spool file (`rb2b.jsonl`):
|
|
39
|
+
|
|
40
|
+
```jsonl
|
|
41
|
+
{"bucket":"company","accountDomain":"globex.com","trigger":"visited pricing 3x","quote":"globex.com — viewed /pricing and /demo, 3 sessions","sourceUrl":"https://app.rb2b.com/v/abc123"}
|
|
42
|
+
{"bucket":"company","accountDomain":"initech.com","trigger":"visited docs","quote":"initech.com — viewed /docs/api","sourceUrl":"https://app.rb2b.com/v/def456"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A row that fails validation (unknown bucket, missing domain/trigger/quote) makes
|
|
46
|
+
the fetch error and names the offending row — a corrupt spool is surfaced, not
|
|
47
|
+
silently skipped. Keep your receiver's mapping faithful to this table.
|
|
48
|
+
|
|
49
|
+
## Where the spool lives
|
|
50
|
+
|
|
51
|
+
`--connector file` with **no path** reads the conventional landing zone:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
<profile home>/signals/spool/ (default: ~/.fullstackgtm/signals/spool/)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Every `*.jsonl` (and `*.json`) file in that directory is read and concatenated,
|
|
58
|
+
name-sorted — so each platform can append to its own file and they all land in
|
|
59
|
+
one fetch:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
~/.fullstackgtm/signals/spool/rb2b.jsonl
|
|
63
|
+
~/.fullstackgtm/signals/spool/trigify.jsonl
|
|
64
|
+
~/.fullstackgtm/signals/spool/hubspot.jsonl
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Point at a different file or directory with `--connector-opt path=<path>`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# the conventional landing zone (zero-arg)
|
|
71
|
+
fullstackgtm signals fetch --connector file --save
|
|
72
|
+
|
|
73
|
+
# a specific file or directory
|
|
74
|
+
fullstackgtm signals fetch --connector file --connector-opt path=./events.jsonl --save
|
|
75
|
+
fullstackgtm signals fetch --connector file --connector-opt path=/var/spool/fsgtm --save
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The CLI **never writes** to the spool — your receiver does. The directory is
|
|
79
|
+
profile-scoped, so spools for different client workspaces stay separate.
|
|
80
|
+
|
|
81
|
+
### Re-reads and retention
|
|
82
|
+
|
|
83
|
+
Re-reading the same spool is safe: signals dedup on `(accountDomain, bucket,
|
|
84
|
+
trigger)` within the configured window (`dedupWindowDays`, default 30), so a row
|
|
85
|
+
that's still in the spool on the next fetch is deduped, not double-counted. The
|
|
86
|
+
CLI does not consume or truncate the spool — **retention is yours**: rotate or
|
|
87
|
+
truncate the files on your own schedule (e.g. nightly, or after a successful
|
|
88
|
+
`--save`). Unbounded spool growth is a self-host concern the CLI doesn't manage.
|
|
89
|
+
|
|
90
|
+
## Writing a receiver
|
|
91
|
+
|
|
92
|
+
A receiver is whatever turns a platform's webhook POST into spool rows. The
|
|
93
|
+
minimum: verify the platform's signature, map the payload to the fields above,
|
|
94
|
+
append one JSON line. Pseudocode:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
// POST handler for your platform's webhook
|
|
98
|
+
function onWebhook(req) {
|
|
99
|
+
verifySignature(req); // platform's HMAC / shared secret
|
|
100
|
+
const row = mapToStagedRow(req.body); // per-platform mapping (below)
|
|
101
|
+
if (!row.quote) return 204; // no verifiable evidence -> skip
|
|
102
|
+
appendLine("~/.fullstackgtm/signals/spool/<platform>.jsonl", JSON.stringify(row));
|
|
103
|
+
return 204;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Keep the receiver dumb: no ranking, no CRM writes, no judgment. All of that is
|
|
108
|
+
the CLI's job, gated and deterministic, on the next `signals fetch`.
|
|
109
|
+
|
|
110
|
+
## Per-platform mapping
|
|
111
|
+
|
|
112
|
+
These map each platform's webhook payload to a staged row. Payload shapes drift —
|
|
113
|
+
verify against the platform's current webhook docs before relying on a field.
|
|
114
|
+
|
|
115
|
+
### RB2B (website visitor de-anonymization)
|
|
116
|
+
|
|
117
|
+
RB2B delivers an identified visitor (person + company) via outbound webhook.
|
|
118
|
+
Use the **company domain** as `accountDomain` and the visited pages as the
|
|
119
|
+
verbatim `quote`.
|
|
120
|
+
|
|
121
|
+
| Spool field | From RB2B payload |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `bucket` | `"company"` (or `"demand"` for high-intent pages) |
|
|
124
|
+
| `accountDomain` | the visitor's company domain |
|
|
125
|
+
| `trigger` | e.g. `"visited pricing"` derived from the top page |
|
|
126
|
+
| `quote` | `"<domain> — <pages viewed>"` (verbatim page paths) |
|
|
127
|
+
| `sourceUrl` | the RB2B visitor permalink |
|
|
128
|
+
|
|
129
|
+
### Trigify (social / job-change listening)
|
|
130
|
+
|
|
131
|
+
Trigify pushes social and job-change events. Map the monitored account's domain;
|
|
132
|
+
the post/job text is the evidence.
|
|
133
|
+
|
|
134
|
+
| Spool field | From Trigify payload |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `bucket` | `"social"` or `"job"` |
|
|
137
|
+
| `accountDomain` | the account/company domain |
|
|
138
|
+
| `trigger` | e.g. `"new role: VP Sales"` or `"posted about <topic>"` |
|
|
139
|
+
| `quote` | the verbatim post text or job title + snippet |
|
|
140
|
+
| `sourceUrl` | the source post / listing URL |
|
|
141
|
+
|
|
142
|
+
### HubSpot form-submission webhook
|
|
143
|
+
|
|
144
|
+
The `hubspot-forms` **pull** connector already covers recent submissions via the
|
|
145
|
+
Forms API (Phase 1). For real-time, subscribe to HubSpot's form-submission
|
|
146
|
+
webhook and append a `demand` row per submission — the same shape the pull
|
|
147
|
+
connector produces, so both paths dedup against each other.
|
|
148
|
+
|
|
149
|
+
| Spool field | From HubSpot submission |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `bucket` | `"demand"` |
|
|
152
|
+
| `accountDomain` | corporate domain of the submitted email (drop free-mail) |
|
|
153
|
+
| `trigger` | `"form: <form name>"` |
|
|
154
|
+
| `quote` | `"Submitted \"<form name>\" — <email>"` |
|
|
155
|
+
| `firstSeen` | submission timestamp (ISO 8601) |
|
|
156
|
+
|
|
157
|
+
## See also
|
|
158
|
+
|
|
159
|
+
- `docs/api.md` — the `signals` verb and the `SignalSourceConnector` contract.
|
|
160
|
+
- The connector taxonomy and the hosted/open-source split for the receiver are
|
|
161
|
+
described in the maintainer design doc (monorepo
|
|
162
|
+
`docs/spec-connectors-signals-outbound.md`).
|
package/docs/tam.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# TAM mapping — size the reachable market, then fill it
|
|
2
|
+
|
|
3
|
+
`tam` answers a longer-horizon question than `market` (the competitive map): **how
|
|
4
|
+
big is the reachable market for my ICP, and how far along am I in actually putting
|
|
5
|
+
it in the CRM?** It is deliberately *not* a one-shot "$10B TAM" headline — it pairs
|
|
6
|
+
a defensible estimate with an iterative, governed population loop and a coverage
|
|
7
|
+
timeline so "this will take a while" becomes a quantified ETA.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
tam estimate → tam populate → (approve/apply each cycle) → tam status --save → tam report
|
|
11
|
+
model schedule governed acquire coverage point burn-up + ETA
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## The model
|
|
15
|
+
|
|
16
|
+
`tam estimate` derives the universe **from your ICP** (`icp.json`):
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
universe accounts = a real count for the ICP filter
|
|
20
|
+
contacts (population target) = accounts × buyers/account
|
|
21
|
+
TAM $ = accounts × ACV (account/per-logo basis — the default)
|
|
22
|
+
= contacts × ACV (buyer/per-seat basis, --acv-basis buyer)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The account count comes from one of three places, always **labeled** in the model:
|
|
26
|
+
|
|
27
|
+
- `--source theirstack` — **the real, technographically-targeted universe** (and the
|
|
28
|
+
only one that returns an actual list). TheirStack counts companies that *use* a
|
|
29
|
+
CRM/MAP — set `firmographics.technologies` (e.g. `["salesforce","hubspot",
|
|
30
|
+
"pipedrive"]`) — plus geo + employee bounds. `accountsSource:
|
|
31
|
+
"provider:theirstack (uses-CRM)"`. This is the buying signal for a CRM-hygiene/
|
|
32
|
+
RevOps tool: "company runs a CRM", not "company is a software publisher". **And
|
|
33
|
+
`tam accounts --source theirstack` pulls the actual company list** (real names +
|
|
34
|
+
domains) you can review, save (`--out <csv>`), and acquire into the CRM.
|
|
35
|
+
(Counting is cheap but **not free** — TheirStack rejects `limit:0`, so a count
|
|
36
|
+
returns 1 row ≈ 3 credits; a list pull is ~3 credits/company. `login theirstack`.)
|
|
37
|
+
- `--source explorium` — a firmographic **company count** from Explorium
|
|
38
|
+
`/v1/businesses` (geo + employee bands + NAICS). A count only — **Explorium 403s
|
|
39
|
+
when you try to pull the list**, and it can't target by CRM-usage, so it's a rough
|
|
40
|
+
size estimate, not a list. `accountsSource: "provider:explorium"`.
|
|
41
|
+
- `--accounts <n>` — an explicit assumption (e.g. a number from a provider export).
|
|
42
|
+
`accountsSource: "assumption"`.
|
|
43
|
+
|
|
44
|
+
> **Why technographic, and why a count isn't enough** (verified live, 2026-06-25/26):
|
|
45
|
+
> lead/people endpoints don't report a real universe — Explorium `/v1/prospects`
|
|
46
|
+
> `total_results` equals the requested page size (1→1, 10→10), and pipe0/Crustdata
|
|
47
|
+
> returns only a pagination cursor with no total. Explorium `/v1/businesses` counts
|
|
48
|
+
> companies (e.g. US mid-market software ≈ 19,000) — **as long as you don't send a
|
|
49
|
+
> `size` field, which silently caps the total** — but it can't filter by CRM-usage
|
|
50
|
+
> and **403s when you try to pull the list**. TheirStack is the technographic
|
|
51
|
+
> source that does both (target by tech, return real records). pipe0 stays a
|
|
52
|
+
> discovery/population source (`tam populate`), not a counting one; `--source pipe0`
|
|
53
|
+
> for `estimate` is rejected with that explanation.
|
|
54
|
+
|
|
55
|
+
Explorium caps `total_results` at **60,000** (a `provider:explorium (≥60k cap —
|
|
56
|
+
floor)` label + a ⚠). TheirStack has no such cap but list-pull costs credits, so
|
|
57
|
+
`tam accounts --max <n>` pages it.
|
|
58
|
+
|
|
59
|
+
### List-pull cost (never spend by surprise)
|
|
60
|
+
|
|
61
|
+
`tam accounts` pulls real records at **~3 TheirStack credits/company**, so it always
|
|
62
|
+
prints the cost up front and guards big pulls:
|
|
63
|
+
|
|
64
|
+
- `tam accounts --dry-run [--usd-per-credit <rate>]` — prices the pull AND the full
|
|
65
|
+
TAM (e.g. *"up to 100 companies ≈ 300 credits. Full TAM (~43,517) ≈ 130,551
|
|
66
|
+
credits (~$5,483) to materialize."*) and spends **0 credits**. The dollar figure
|
|
67
|
+
appears only when you give a rate — TheirStack pricing is ~$0.04–$0.11/credit by
|
|
68
|
+
tier, and the tool won't fabricate a default.
|
|
69
|
+
- A pull above `--max-credits` (default **150** ≈ 50 companies) requires `--confirm`
|
|
70
|
+
— so a large pull is always a deliberate choice, not an accident.
|
|
71
|
+
|
|
72
|
+
So the natural pattern is **count freely, pull in confirmed batches** (`--max N`),
|
|
73
|
+
acquire, measure coverage, repeat — rather than one large up-front spend.
|
|
74
|
+
|
|
75
|
+
**ACV must be a real, ANNUAL figure that you confirm** — never a band default, and
|
|
76
|
+
never silently lifted from the CRM. `--provider` is your **coverage** source, not
|
|
77
|
+
your ACV: the account universe is Explorium, coverage is the CRM, and the TAM's
|
|
78
|
+
economics are yours to confirm. Two confirmed paths, always labeled (`acv.source`):
|
|
79
|
+
|
|
80
|
+
- `--acv <annual-usd>` — your confirmed annualized ACV (`source: "explicit (annual)"`).
|
|
81
|
+
- `--acv-from-crm --deal-period monthly|quarterly|annual` — derive the **median
|
|
82
|
+
closed-won deal amount** and **annualize** it by the period you specify
|
|
83
|
+
(`source: "crm:closed-won (N deals, median $X/monthly ×12 = annual)"`). The period
|
|
84
|
+
is **required** because a deal amount can be MRR, quarterly, or annual — guessing
|
|
85
|
+
it is a 4–12× error (a $15k/mo deal is a $180k ACV). The deal count is in the
|
|
86
|
+
label, so a thin signal (e.g. "1 deal") is visible — don't over-trust it.
|
|
87
|
+
|
|
88
|
+
If you give neither, `estimate` errors rather than invent a number — and a bare
|
|
89
|
+
`--provider` does NOT auto-set ACV.
|
|
90
|
+
|
|
91
|
+
**Buyers/account** (the contact population target = accounts × buyers) gets the same
|
|
92
|
+
treatment: `--buyers-per-account <n>`, else the CRM's **average contacts per account**
|
|
93
|
+
(`crm:avg-contacts/account (N)`), else a clearly-labeled minimal assumption of 1
|
|
94
|
+
(`assumption:1-buyer`) — never a silent guess. `--acv-basis buyer` makes ACV per-seat
|
|
95
|
+
(TAM = contacts × ACV) instead of per-logo.
|
|
96
|
+
|
|
97
|
+
Optional citable **cross-checks** (`--cross-checks <file.json>`, an array of
|
|
98
|
+
`{claim, valueUsd?, sourceUrl, quote, asOf?}`) sit alongside the bottom-up number as a
|
|
99
|
+
top-down sanity check — the market-research flavor, evidence-attached.
|
|
100
|
+
|
|
101
|
+
`estimate` also records a **coverage baseline** (the CRM account/contact counts right
|
|
102
|
+
now, when a `--provider`/`--input`/`--demo` source is given) so `status` can attribute
|
|
103
|
+
what *this campaign* added versus what was already there.
|
|
104
|
+
|
|
105
|
+
## Populate it (governed, never auto-writes)
|
|
106
|
+
|
|
107
|
+
`tam populate --cron "<expr>"` wires a recurring **`enrich acquire --source <s> --save`**
|
|
108
|
+
into the scheduler. Each firing queues a `needs_approval` lead plan — it does **not**
|
|
109
|
+
write to the CRM, and the acquire meter is charged only at apply. Apply stays a separate
|
|
110
|
+
human gate (`apply --plan-id <id>`, re-checked approved at every firing). So a scheduled
|
|
111
|
+
TAM populates by *accumulating proposals you approve*, never by surprise leads.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
fullstackgtm tam populate --cron "0 7 * * 1-5" --source pipe0 --provider hubspot
|
|
115
|
+
fullstackgtm schedule install # activate the crontab entry
|
|
116
|
+
# each cycle:
|
|
117
|
+
fullstackgtm plans list # the morning's lead plan
|
|
118
|
+
fullstackgtm plans approve <id> --operations all
|
|
119
|
+
fullstackgtm apply --plan-id <id> --provider hubspot
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This is the one place the schedule allowlist permits a create-side verb, and only in
|
|
123
|
+
its plan-producing `--save` form (scheduling `enrich acquire` without `--save` is
|
|
124
|
+
rejected — it would queue nothing).
|
|
125
|
+
|
|
126
|
+
## Track coverage (where you're getting to)
|
|
127
|
+
|
|
128
|
+
Coverage is measured on the **intersection** of your CRM and the TAM's ICP — not a
|
|
129
|
+
raw account count. `tam estimate` stores the ICP filter on the model, and `tam
|
|
130
|
+
status` classifies every domain-bearing CRM account against it:
|
|
131
|
+
|
|
132
|
+
- **in-TAM** — matches every CRM-checkable criterion (size, industry) with no
|
|
133
|
+
contradiction. *Only these count toward coverage.*
|
|
134
|
+
- **out-of-TAM** — contradicts a criterion (wrong size/industry). Reported
|
|
135
|
+
separately; never inflates coverage. So 50k accounts loaded from other sources
|
|
136
|
+
that aren't your target market don't fake a high number.
|
|
137
|
+
- **unknown** — the record lacks the fields to judge. Surfaced, never silently
|
|
138
|
+
counted — it tells you what to enrich.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
fullstackgtm tam status --provider hubspot --save # classified coverage + a timeline reading
|
|
142
|
+
fullstackgtm tam report # markdown: model + in/out/unknown + burn-up + ETA
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **Honest limit:** geo and "uses a CRM" aren't on a CRM account record, so an
|
|
146
|
+
> in-TAM classification means "matches what the CRM lets us check" (size + industry),
|
|
147
|
+
> not a full technographic match. A `--reverify` pass (re-confirm geo/tech per
|
|
148
|
+
> domain via TheirStack) is planned; until then, status labels what it checked.
|
|
149
|
+
|
|
150
|
+
**Bottom-up vs top-down — squaring "more in CRM than estimated."** The estimate is a
|
|
151
|
+
*lower-bound prior*, not a ceiling — TheirStack only knows ~740k US firms, so your
|
|
152
|
+
multi-source CRM can legitimately hold more valid in-TAM accounts than it predicted.
|
|
153
|
+
Every in-TAM account is a *proven* member of the real market, so the in-TAM count is
|
|
154
|
+
the strongest floor on the true universe. When `in-TAM ≥ estimate`, `status` stops
|
|
155
|
+
reporting a fake "100%" and flips the signal: **"the estimate was a floor — your real
|
|
156
|
+
market is at least N; re-estimate (broader ICP / better source) to find the
|
|
157
|
+
headroom."** `reconciledUniverse = max(estimate, in-TAM)`.
|
|
158
|
+
|
|
159
|
+
Once two-plus readings show movement, `report`/`status` project an **ETA** on the
|
|
160
|
+
**in-TAM** burn rate (off-ICP accounts loaded elsewhere don't count as progress) →
|
|
161
|
+
days remaining → a projected fill date. Fewer than two readings, a flat rate, or no
|
|
162
|
+
elapsed time returns an honest "not enough history yet," never a fabricated date.
|
|
163
|
+
|
|
164
|
+
## Storage
|
|
165
|
+
|
|
166
|
+
Profile-scoped under `<home>/tam/<name>/`:
|
|
167
|
+
|
|
168
|
+
- `model.json` — the TAM model (universe, ACV, cross-checks, baseline). 0600.
|
|
169
|
+
- `coverage.jsonl` — append-only coverage readings (one per `status --save`). 0600.
|
|
170
|
+
|
|
171
|
+
Scope per client with `--profile <name>`; name multiple TAMs with `--name <n>`
|
|
172
|
+
(default `default`).
|
|
173
|
+
|
|
174
|
+
## Invariants
|
|
175
|
+
|
|
176
|
+
- **Estimate is honest about its source.** Every model says whether the account count
|
|
177
|
+
is a `provider:*` probe or an `assumption`; the probe errors rather than silently
|
|
178
|
+
guessing when the provider returns no count.
|
|
179
|
+
- **No fabricated economics.** ACV must be explicit or CRM-derived (closed-won) — there
|
|
180
|
+
are no band defaults, and `estimate` refuses without a real ACV. Buyers/account and
|
|
181
|
+
ACV both carry a `source` label so a thin signal is visible, never hidden.
|
|
182
|
+
- **Population never auto-writes.** `tam populate` schedules plan-only acquire; apply
|
|
183
|
+
is a separate gate, the meter is charged only at apply, and the allowlist requires
|
|
184
|
+
`--save`.
|
|
185
|
+
- **Coverage is deterministic** from the CRM snapshot + the model baseline — no LLM,
|
|
186
|
+
byte-stable, comparable run-over-run.
|
|
187
|
+
- **ETA is refused when unprojectable** rather than faked.
|
|
188
|
+
|
|
189
|
+
## Library
|
|
190
|
+
|
|
191
|
+
`estimateTam`, `computeCoverage`, `coverageCountsFromSnapshot`, `projectEta`,
|
|
192
|
+
`tamReportToMarkdown`, the store (`saveTamModel`/`loadTamModel`/`appendCoverage`/
|
|
193
|
+
`readCoverageTimeline`), the count probe (`probeExploriumBusinessCount` +
|
|
194
|
+
`EXPLORIUM_BUSINESS_COUNT_CAP`), and the ICP→firmographic filter
|
|
195
|
+
(`icpToExploriumBusinessFilters`) are all exported for programmatic use.
|
package/llms.txt
CHANGED
|
@@ -20,6 +20,7 @@ at/above `--fail-on`.
|
|
|
20
20
|
- [Contributing](https://github.com/fullstackgtm/core/blob/main/CONTRIBUTING.md): dev setup, the open-core mirror model, the release ritual
|
|
21
21
|
- [API reference](https://github.com/fullstackgtm/core/blob/main/docs/api.md): semver-covered surfaces — canonical model, rule interface, plan/apply contract, connector contract, config, CLI, MCP tools
|
|
22
22
|
- [CRM-health lifecycle](https://github.com/fullstackgtm/core/blob/main/docs/crm-health-lifecycle.md): the Prevent → Detect → Remediate → Verify/Attribute model; no-new-dupes design
|
|
23
|
+
- [TAM mapping](https://github.com/fullstackgtm/core/blob/main/docs/tam.md): size the reachable market from the ICP, then populate it with scheduled governed acquire and track coverage to an ETA
|
|
23
24
|
- [CHANGELOG](https://github.com/fullstackgtm/core/blob/main/CHANGELOG.md): release history
|
|
24
25
|
|
|
25
26
|
`fullstackgtm resolve <type>` is the create gate: exit 0 = safe to create,
|
|
@@ -58,6 +59,60 @@ uncertainty; report bubbles ∝ estimated share only when signals exist.
|
|
|
58
59
|
Storage is profile-scoped under `<home>/market/<category>`. MCP:
|
|
59
60
|
`fullstackgtm_market_worksheet`, `fullstackgtm_market_observe`.
|
|
60
61
|
|
|
62
|
+
## Key invariants (TAM mapping)
|
|
63
|
+
|
|
64
|
+
`fullstackgtm tam` sizes the reachable market FROM the ICP and then iteratively
|
|
65
|
+
fills it — not a one-shot headline. `tam estimate` computes a transparent model:
|
|
66
|
+
account universe × ACV = TAM (account/per-logo basis by default; `--acv-basis
|
|
67
|
+
buyer` for per-seat = contacts × ACV), where contacts = accounts ×
|
|
68
|
+
buyers/account is the population target. The account count is `--accounts <n>`
|
|
69
|
+
(assumption), OR `--source theirstack` — the TECHNOGRAPHIC universe: counts
|
|
70
|
+
companies that USE a CRM/MAP (`firmographics.technologies` → TheirStack
|
|
71
|
+
`company_technology_slug_or`) + geo + employee bounds; the real RevOps buying signal
|
|
72
|
+
AND the only source that returns a LIST (`tam accounts --source theirstack` pulls
|
|
73
|
+
real companies name/domain, `--out csv`/`--json`, `--max` caps credit spend; a
|
|
74
|
+
count is cheap but NOT free — limit:0 is rejected so a count returns 1 row ≈ 3
|
|
75
|
+
credits, a list pull ≈ 3/company; `login theirstack`). OR `--source explorium` — a firmographic
|
|
76
|
+
count via `/v1/businesses` (`total_results`), a count ONLY (Explorium 403s on
|
|
77
|
+
list-pull, can't target by CRM-usage; must OMIT `size` or it caps the total; capped
|
|
78
|
+
at 60,000 `EXPLORIUM_BUSINESS_COUNT_CAP` → `provider:explorium (≥60k cap — floor)` +
|
|
79
|
+
⚠). Verified live: people endpoints can't size a market (Explorium `/v1/prospects`
|
|
80
|
+
total == page size; pipe0/Crustdata returns only a cursor — `--source pipe0` for
|
|
81
|
+
estimate is REJECTED, it's a population source). The model always LABELS
|
|
82
|
+
`accountsSource` (provider:theirstack/explorium vs assumption). ACV must be a
|
|
83
|
+
real ANNUAL figure the operator CONFIRMS — `--acv <annual-usd>`, OR `--acv-from-crm
|
|
84
|
+
--deal-period monthly|quarterly|annual` which takes the median closed-won amount and
|
|
85
|
+
ANNUALIZES it (the period is required — a deal can be MRR/quarterly/annual and
|
|
86
|
+
guessing is a 4–12× error; $15k/mo → $180k). NO band defaults, and a bare
|
|
87
|
+
`--provider` does NOT auto-set ACV (HubSpot is the COVERAGE source, not the ACV) —
|
|
88
|
+
`estimate` REFUSES without a confirmed ACV. Buyers/account is `--buyers-per-account`
|
|
89
|
+
OR the CRM's avg-contacts-per-account OR a labeled `assumption:1-buyer`. Both
|
|
90
|
+
`acv.source` and `universe.buyersSource` are labeled (incl. deal count + the
|
|
91
|
+
period×factor, so a thin signal like "1 deal" is visible). Optional
|
|
92
|
+
citable cross-checks (`{claim,valueUsd?,sourceUrl,quote}`) sit beside the
|
|
93
|
+
bottom-up number as a top-down sanity check. `estimate`
|
|
94
|
+
records a coverage BASELINE (CRM counts now) so `status` attributes what the
|
|
95
|
+
campaign added. `tam populate --cron` schedules `enrich acquire --source <s>
|
|
96
|
+
--save` — the ONE create-side verb the schedule allowlist permits, and only in
|
|
97
|
+
its plan-producing `--save` form: each firing queues a needs_approval lead plan,
|
|
98
|
+
the acquire meter is charged only at apply, and apply stays `apply --plan-id`
|
|
99
|
+
(re-checked approved). `tam status` measures coverage on the INTERSECTION of CRM ×
|
|
100
|
+
TAM-ICP, not a raw count: `tam estimate` stores the ICP filter (`model.targeting`),
|
|
101
|
+
and status CLASSIFIES each domain-bearing account into in-TAM / out-of-TAM / unknown
|
|
102
|
+
(checked on size+industry — geo + "uses-CRM" aren't on a CanonicalAccount, so a
|
|
103
|
+
`--reverify` re-enrich is future; the classifier labels what it checked). ONLY
|
|
104
|
+
in-TAM counts toward coverage, so off-ICP accounts loaded from elsewhere can't
|
|
105
|
+
inflate it. Bottom-up vs top-down reconciliation: the in-TAM count is a FLOOR on the
|
|
106
|
+
real universe (those are verified members), so when in-TAM ≥ the estimate, status
|
|
107
|
+
stops reporting a fake 100% and flips to "estimate was a floor — real market is at
|
|
108
|
+
least N (`reconciledUniverse = max(estimate, inTam)`); re-estimate for the headroom".
|
|
109
|
+
`--save` stamps an append-only timeline. `tam report` (markdown) and `status` project
|
|
110
|
+
an ETA on the IN-TAM accounts/day burn across the timeline → days-remaining → fill
|
|
111
|
+
date — and REFUSE to project (honest "not enough history") below two readings, a flat
|
|
112
|
+
rate, or zero elapsed time.
|
|
113
|
+
Coverage + ETA are deterministic (no LLM, byte-stable). Storage profile-scoped
|
|
114
|
+
under `<home>/tam/<name>/` (`model.json` + `coverage.jsonl`, 0600).
|
|
115
|
+
|
|
61
116
|
## Key invariants (governed write verbs)
|
|
62
117
|
|
|
63
118
|
`bulk-update <object> --where … (--set|--archive|--create-task)` filters the
|
|
@@ -123,8 +178,12 @@ and no webhook ingestion (Phase 2). Never auto-writes.
|
|
|
123
178
|
The timing/outbound layer turns "who changed" into one grounded, governed
|
|
124
179
|
opener — and **never sends**. `signals fetch` captures fresh buying triggers
|
|
125
180
|
into a profile-scoped ledger (free Greenhouse/Lever/Ashby ATS scrapers in the
|
|
126
|
-
box; funding/social via
|
|
127
|
-
|
|
181
|
+
box; funding/social via `--from`; connected platforms via `--connector`
|
|
182
|
+
file/serpapi-news/hubspot-forms with secrets through the credential ladder; and
|
|
183
|
+
push platforms via the webhook spool — `--connector file` reads the conventional
|
|
184
|
+
landing zone `<home>/signals/spool/*.jsonl`, see docs/signal-spool-format.md),
|
|
185
|
+
keyed by **account domain**, ranked by learned `weights`; it writes NOTHING to
|
|
186
|
+
the CRM. `icp interview|set|show`
|
|
128
187
|
builds `icp.json`; `icp judge` ranks fresh signals into send/nurture/skip — pass
|
|
129
188
|
a snapshot (`--provider`/`--input`/`--demo`) and each decision resolves its CRM
|
|
130
189
|
target: `accountId`, the best `contact {id,email,title}`, and `contacts[]` (all
|
|
@@ -135,8 +194,11 @@ bar). `draft` emits ONE trigger-grounded opener per hot account as a governed
|
|
|
135
194
|
`create_task` targeting the resolved `contact.id` (or `accountId`) — a
|
|
136
195
|
domain-only decision (account not yet in the CRM) is REJECTED with "acquire it
|
|
137
196
|
first" (run `enrich acquire`, re-judge with the snapshot). `--channel
|
|
138
|
-
email|linkedin|task` shapes the opener
|
|
139
|
-
|
|
197
|
+
email|linkedin|task` shapes the opener. After `plans approve`, applying the draft
|
|
198
|
+
plan logs the touch as a CRM task (`apply --provider <crm>`) OR renders it to the
|
|
199
|
+
governed outbox (`apply --channel outbox` → `<home>/signals/outbox/<channel>.jsonl`
|
|
200
|
+
for a downstream sender to drain). Either way the CLI **transmits nothing** — the
|
|
201
|
+
outbox is the send-side mirror of the spool; the always-on sender is hosted/opt-in. `signals outcome --account <domain>
|
|
140
202
|
--contact <id> --result replied|...` credits the contact reached so weights
|
|
141
203
|
re-learn. The contact↔account join (`contact.accountId` + `account.domain`) is
|
|
142
204
|
surfaced at every hop, so the loop is contact-coherent end to end — see the
|
|
@@ -147,7 +209,8 @@ surfaced at every hop, so the loop is contact-coherent end to end — see the
|
|
|
147
209
|
`fullstackgtm schedule` is the horizontal scheduler — no feature namespace
|
|
148
210
|
owns cron logic. `add "<command>" --cron "<expr>"` validates the command
|
|
149
211
|
against the read/plan-side allowlist (audit, snapshot, enrich append|refresh,
|
|
150
|
-
market capture|refresh,
|
|
212
|
+
enrich acquire --save, market capture|refresh, signals fetch, icp judge|eval,
|
|
213
|
+
draft, suggest, report, doctor) and the 5-field cron
|
|
151
214
|
expression, but touches no crontab; `install` materializes enabled entries
|
|
152
215
|
into a sentinel-delimited managed block (`# >>> fullstackgtm <profile> >>>`)
|
|
153
216
|
replaced wholesale on re-install, lines outside it untouched; `uninstall`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
|
|
@@ -63,10 +63,11 @@ credentials AND stored plans per client org.
|
|
|
63
63
|
| `call parse\|score\|link\|plan` | Transcripts → evidence-quoted insights, rubric scorecards, deal linking, governed next-step writes |
|
|
64
64
|
| `enrich append\|refresh\|ingest\|status` | Governed enrichment (Apollo pull / Clay ingest), fill-blanks-only plans |
|
|
65
65
|
| `enrich acquire [--source explorium\|pipe0\|linkedin]` | Net-new ICP-targeted lead gen: resolve-first deduped `create_record` plans, capped by a per-profile windowed meter (records + spend); owner-stamped via `acquire.assign`/`--assign-owner` (never born ownerless); links each lead to a domain-stamped, signal-watchable account; `acquire.create.company` acquires accounts (ABM); `--source linkedin --list <id>` reads a HeyReach lead list (Phase 1 discovery, read-only — never sends); never auto-writes |
|
|
66
|
-
| `signals fetch\|list\|outcome\|weights` | Detect-side timing layer: capture fresh buying triggers into a profile-scoped ledger (free Greenhouse/Lever/Ashby ATS in the box;
|
|
66
|
+
| `signals fetch\|list\|outcome\|weights` | Detect-side timing layer: capture fresh buying triggers into a profile-scoped ledger (free Greenhouse/Lever/Ashby ATS in the box; `--from` staged files; `--connector file\|serpapi-news\|hubspot-forms` to pull connected platforms / read the webhook spool `<home>/signals/spool`, secrets via the credential ladder), ranked by learned weights. Writes NOTHING to the CRM; `outcome --account <domain> --contact <id> --result …` re-weights which triggers earn a touch |
|
|
67
67
|
| `icp interview\|set\|show\|judge\|eval` | Build the ICP, then `judge` ranks fresh signals into send/nurture/skip — pass a snapshot (`--provider`/`--input`) and each decision resolves its CRM target (`accountId` + the `contact`/`contacts` to reach). `eval` is the calibration gate (exit 2 below the bar) |
|
|
68
|
-
| `draft [--from-judge latest] [--channel email\|linkedin\|task]` | One trigger-grounded opener per hot account → a governed `create_task` targeting the resolved `contact.id` (or `accountId`); rejects a domain-only decision ("acquire it first"). **Never sends** —
|
|
68
|
+
| `draft [--from-judge latest] [--channel email\|linkedin\|task]` | One trigger-grounded opener per hot account → a governed `create_task` targeting the resolved `contact.id` (or `accountId`); rejects a domain-only decision ("acquire it first"). **Never sends** — after `plans approve`, `apply --provider <crm>` logs it as a CRM task, or `apply --channel outbox` renders it to `<home>/signals/outbox/<channel>.jsonl` for a downstream sender to drain; the CLI transmits nothing |
|
|
69
69
|
| `market init\|capture\|classify\|worksheet\|observe\|fronts\|axes\|overlay\|scale\|report\|refresh` | Competitive category map; evidence quotes verified verbatim against stored captures |
|
|
70
|
+
| `tam estimate\|accounts\|status\|report\|populate` | Size the reachable market FROM the ICP (real account count × real ACV; buyers/account → contact target), then iteratively fill it: `populate` schedules plan-only `enrich acquire --save` (apply stays gated), `status` classifies CRM accounts vs the TAM ICP into **in-TAM / out-of-TAM / unknown** (only in-TAM counts — junk loaded from elsewhere doesn't inflate coverage; checked on size+industry, geo/tech need re-enrich) and flips to "estimate was a FLOOR, revise up" when bottom-up in-CRM exceeds the top-down estimate; `--save` stamps a timeline, `report` projects burn-up + ETA on the in-TAM rate. **`estimate --source theirstack`** counts companies that USE a CRM/MAP (`firmographics.technologies`) — the real RevOps universe — and **`tam accounts --source theirstack`** pulls the actual company LIST (names+domains, `--out csv`) at ~3 credits/company — `--dry-run [--usd-per-credit <r>]` prices the pull + full TAM for 0 credits, `--confirm` gates pulls above `--max-credits` (default 150). `--source explorium` = firmographic count only (no list, 60k cap = floor); else `--accounts`. **ACV must be a real ANNUAL figure you confirm** — `--acv <annual-usd>`, or `--acv-from-crm --deal-period monthly\|quarterly\|annual` (median closed-won, annualized); NO band defaults, a bare `--provider` does NOT set ACV (HubSpot=coverage), refuses without one. Buyers/account = CRM avg-contacts or explicit. Every number labeled with its source |
|
|
70
71
|
| `schedule add\|list\|remove\|enable\|disable\|run\|install\|uninstall\|status` | Horizontal cron; read/plan-side allowlist only — scheduling NEVER auto-approves |
|
|
71
72
|
| `report` | Client-ready audit deliverable (markdown or self-contained HTML) |
|
|
72
73
|
| `health [--json]` | Per-profile CRM health timeline: deterministic 0–100 score, trend, per-rule deltas — accrues from `audit --save`, read-only |
|