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.
Files changed (47) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +18 -7
  3. package/dist/cli.js +634 -56
  4. package/dist/connectors/outboxChannel.d.ts +64 -0
  5. package/dist/connectors/outboxChannel.js +170 -0
  6. package/dist/connectors/prospectSources.d.ts +22 -0
  7. package/dist/connectors/prospectSources.js +43 -0
  8. package/dist/connectors/signalSources.d.ts +105 -0
  9. package/dist/connectors/signalSources.js +316 -0
  10. package/dist/connectors/theirstack.d.ts +84 -0
  11. package/dist/connectors/theirstack.js +125 -0
  12. package/dist/icp.d.ts +47 -3
  13. package/dist/icp.js +105 -11
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -0
  16. package/dist/init.js +3 -0
  17. package/dist/judgeEval.d.ts +7 -0
  18. package/dist/judgeEval.js +8 -1
  19. package/dist/runReport.d.ts +26 -0
  20. package/dist/runReport.js +15 -0
  21. package/dist/schedule.js +18 -4
  22. package/dist/signals.d.ts +54 -0
  23. package/dist/signals.js +64 -0
  24. package/dist/tam.d.ts +225 -0
  25. package/dist/tam.js +470 -0
  26. package/docs/api.md +18 -4
  27. package/docs/outbox-format.md +92 -0
  28. package/docs/recipes.md +37 -0
  29. package/docs/roadmap-to-1.0.md +31 -1
  30. package/docs/signal-spool-format.md +162 -0
  31. package/docs/tam.md +195 -0
  32. package/llms.txt +68 -5
  33. package/package.json +1 -1
  34. package/skills/fullstackgtm/SKILL.md +3 -2
  35. package/src/cli.ts +714 -51
  36. package/src/connectors/outboxChannel.ts +202 -0
  37. package/src/connectors/prospectSources.ts +57 -0
  38. package/src/connectors/signalSources.ts +363 -0
  39. package/src/connectors/theirstack.ts +170 -0
  40. package/src/icp.ts +113 -11
  41. package/src/index.ts +32 -0
  42. package/src/init.ts +3 -0
  43. package/src/judgeEval.ts +8 -1
  44. package/src/runReport.ts +39 -0
  45. package/src/schedule.ts +20 -4
  46. package/src/signals.ts +90 -0
  47. package/src/tam.ts +654 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,199 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md)
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.44.0] — 2026-07-01
11
+
12
+ ### Added
13
+
14
+ - **Paired CLI auto-links HubSpot to the hosted deployment.** When a broker
15
+ credential exists (a paired CLI), `audit --provider hubspot` hands the local
16
+ HubSpot private-app token to the deployment over the broker channel
17
+ (best-effort, with a printed notice), so the hosted Integrations page shows
18
+ HubSpot connected and syncing without a separate OAuth setup. Unpaired CLIs
19
+ are unaffected — pairing remains opt-in via `login --via`.
20
+
21
+ ### Added
22
+
23
+ - **`tam accounts` credit-cost preview + spend guard.** Pulling the list costs ~3
24
+ TheirStack credits/company, so `tam accounts` now prints the cost up front and
25
+ never spends by surprise: `--dry-run [--usd-per-credit <rate>]` prices the pull
26
+ AND the full TAM (e.g. "up to 100 ≈ 300 credits; full TAM ~43,517 ≈ 130,551
27
+ credits (~$5,483)") for **0 credits**, and a pull above `--max-credits` (default
28
+ 150 ≈ 50 companies) requires `--confirm`. The dollar figure appears only with an
29
+ explicit rate (no fabricated default; TheirStack is ~$0.04–$0.11/credit by tier).
30
+ `tam populate` also notes where spend lives — the acquire meter governs
31
+ population, separate from the TheirStack list pull. Library:
32
+ `theirStackPullCost` + `THEIRSTACK_CREDITS_PER_COMPANY`.
33
+
34
+ ### Changed
35
+
36
+ - **`tam status` coverage is now classified against the TAM ICP — not a raw
37
+ account count.** Previously it counted *every* domain-bearing CRM account over
38
+ the universe, so accounts loaded from any source (off-ICP or not) inflated
39
+ coverage and could read >100%. Now `tam estimate` stores the ICP filter
40
+ (`model.targeting`), and `status` classifies each account into **in-TAM /
41
+ out-of-TAM / unknown** (checked on size + industry; geo and "uses-CRM" aren't on
42
+ a `CanonicalAccount`, so they need re-enrichment — a `--reverify` pass is
43
+ planned, and the classifier labels what it checked). Only in-TAM counts toward
44
+ coverage; off-ICP junk is bucketed out. **Bottom-up vs top-down reconciliation:**
45
+ the in-TAM count is a floor on the real universe, so when it meets/exceeds the
46
+ estimate, `status` stops reporting a fake 100% and flips to "the estimate was a
47
+ floor — your real market is at least N (`reconciledUniverse`); re-estimate for
48
+ the headroom." ETA now burns on in-TAM accounts/day. New library:
49
+ `classifyAccount`, `classifyCoverage`, `crmCheckableCriteria`, `coveredAccounts`,
50
+ `TamTargeting`/`TamClassified` (and `TamModel.targeting`). Legacy models without
51
+ targeting fall back to counting all accounts, with a "re-estimate to classify"
52
+ note.
53
+
54
+ ### Added
55
+
56
+ - **`tam` technographic sourcing via TheirStack — target by CRM-usage, get a real
57
+ list.** The Explorium firmographic count (NAICS/size/geo) is a weak proxy for a
58
+ RevOps/CRM-hygiene tool and can't return the list (it 403s on pull). New
59
+ `--source theirstack` counts companies that actually **use** a CRM/MAP — set
60
+ `icp.firmographics.technologies` (e.g. `["salesforce","hubspot","pipedrive"]`),
61
+ mapped to TheirStack `company_technology_slug_or` + employee bounds + country —
62
+ which is the real buying signal, labeled `provider:theirstack (uses-CRM)`. And a
63
+ new **`tam accounts --source theirstack`** pulls the actual company list (real
64
+ names + domains, `--out <csv>` or `--json`, `--max` caps the credit spend), so the
65
+ TAM is a list you can review and acquire, not just a number. Counting is cheap
66
+ but not free (TheirStack rejects `limit:0`, so a count returns 1 row ≈ 3 credits;
67
+ a list pull is ~3 credits/company). New connector
68
+ `connectors/theirstack.ts` (`theirStackCountCompanies` / `theirStackSearchCompanies`),
69
+ `icpToTheirStackFilters` + `employeeBandsToRange`, and `login theirstack`.
70
+ (Verified live: filters + `metadata.total_results`; `limit` must be ≥ 1.)
71
+
72
+ ### Changed
73
+
74
+ - **`tam estimate` requires a confirmed ANNUAL ACV — no fabricated defaults, and
75
+ the CRM is not silently the ACV source.** A TAM dollar figure built on a guessed
76
+ ACV is a false signal, so the `--acv-band smb|mid|enterprise` presets (hardcoded
77
+ $6k/$24k/$90k) are removed and `estimate` refuses to run without a real ACV. Two
78
+ confirmed paths, always labeled on the model (`acv.source`): `--acv <annual-usd>`
79
+ (`"explicit (annual)"`), or `--acv-from-crm --deal-period monthly|quarterly|annual`
80
+ — the median closed-won deal amount **annualized** by the stated period
81
+ (`"crm:closed-won (N deals, median $X/monthly ×12 = annual)"`). The period is
82
+ **required**: a deal amount can be MRR/quarterly/annual and guessing it is a
83
+ 4–12× error (a $15k/mo deal is a $180k ACV). A bare `--provider` is the COVERAGE
84
+ source and **no longer auto-sets ACV**. **Buyers/account** likewise:
85
+ `--buyers-per-account <n>`, else the CRM's **average contacts per account**
86
+ (`"crm:avg-contacts/account (N)"`), else a labeled `"assumption:1-buyer"` — never
87
+ a silent default of 3. New library helpers `deriveAcvFromClosedWon` /
88
+ `deriveBuyersPerAccount`; `TamModel.acv.source` and `universe.buyersSource` are
89
+ new fields (and `acv.band` is gone).
90
+
91
+ ### Fixed
92
+
93
+ - **`icp eval --golden default` no longer rots with the calendar.** The default
94
+ golden set's `firstSeen` stamps are relative to a pinned instant
95
+ (`DEFAULT_GOLDEN_NOW_ISO`, exported), but grading used wall-clock `new Date()`
96
+ — so freshness decay silently degraded the "fresh → send" rows until the gate
97
+ started failing (~8 days after the set was authored) on every fresh install
98
+ and in CI, with no code change. The default set is now graded on its own
99
+ pinned clock; file-based golden sets still grade against wall time (their
100
+ timestamps are the caller's).
101
+
102
+ - **`enrich acquire` (pipe0/Crustdata) over-narrow ICP filter → zero discovery.**
103
+ Live testing surfaced that a RevOps ICP returned 0 prospects from pipe0/Crustdata
104
+ even though a title-only search returned plenty — the prime suspect is the
105
+ industry vocabulary: LinkedIn renamed its industry taxonomy (v1→v2) and the map
106
+ sent only one generation, so a vendor on the other generation matches nothing.
107
+ `CRUSTDATA_INDUSTRY` now sends BOTH generations per cluster (e.g. "Software
108
+ Development" *and* "Computer Software"), OR-matched within the field. (pipe0
109
+ credits were exhausted mid-investigation, so this is a reasoned fix not yet
110
+ re-confirmed end-to-end — re-run `enrich acquire --source pipe0` once credits
111
+ refill; if still zero, the next suspects are the `current_seniority_levels`
112
+ shape and `locations`. Note `scoreProspectAgainstIcp` backstops persona but NOT
113
+ industry, so the industry filter is load-bearing.)
114
+ - **`enrich acquire` now surfaces a zero-discovery result** instead of emitting a
115
+ silent empty plan: when a provider returns 0 prospects it warns that this is
116
+ usually an over-narrow filter (not an empty market) and points at `icp show`;
117
+ and when all discovered prospects score below the fit threshold it says so.
118
+
119
+ ### Added
120
+
121
+ - **`tam` — Total Addressable Market mapping.** Size the reachable market FROM
122
+ the ICP, then iteratively fill it and track coverage to an ETA. `tam estimate`
123
+ computes a transparent model (account universe × ACV = TAM, account/per-logo
124
+ basis by default or `--acv-basis buyer`; contacts = accounts × buyers/account
125
+ is the population target). The account count is `--accounts <n>` (assumption)
126
+ or `--source explorium` — a live count of matching **companies** via Explorium
127
+ `/v1/businesses`, which is the account universe directly. (Verified against the
128
+ live APIs: people/lead endpoints can't size a market — Explorium `/v1/prospects`
129
+ `total_results` equals the page size and pipe0/Crustdata returns only a cursor —
130
+ so `--source pipe0` for estimate is rejected; pipe0 remains a discovery/
131
+ population source. The businesses count omits `size`, which otherwise caps the
132
+ total.) Explorium caps the count at 60,000; a saturated reading is surfaced as a
133
+ floor (`provider:explorium (≥60k cap — floor)` + a warning to narrow the ICP).
134
+ The model always labels `accountsSource` provider-vs-assumption. Optional citable
135
+ cross-checks sit beside the bottom-up number. `tam populate --cron` schedules
136
+ governed `enrich acquire --save` (plan-only; see below). `tam status --save`
137
+ stamps a coverage timeline (CRM accounts-with-domain + contacts vs. the
138
+ universe, `$ covered` proportional, what the campaign added since baseline);
139
+ `tam report` renders a markdown burn-up. `status`/`report` project a linear
140
+ accounts/day ETA and refuse to project (honest "not enough history") below two
141
+ readings or a flat rate. Coverage + ETA are deterministic (no LLM). Library:
142
+ `estimateTam`, `computeCoverage`, `projectEta`, `tamReportToMarkdown`, the
143
+ store, and the count probe (`probeExploriumBusinessCount` +
144
+ `EXPLORIUM_BUSINESS_COUNT_CAP`). See [docs/tam.md](./docs/tam.md).
145
+ - **Source connectors for `signals` (Phase 1).** `signals fetch` gains an
146
+ additive, opt-in `--connector <id>[,<id>] [--connector-opt k=v …]` that pulls
147
+ signals from connected platforms instead of only ATS boards and hand-staged
148
+ `--from` files. Three connectors ship: `file` (local JSON/JSONL — also the
149
+ webhook landing-zone spool, no auth), `serpapi-news` (funding/company news via
150
+ a news REST API, key through the credential ladder), and `hubspot-forms`
151
+ (first-party `demand` from recent form submissions, reusing the existing
152
+ HubSpot login). Secrets resolve through the env → login → broker ladder, never
153
+ argv; `--connector-opt` carries non-secret knobs only. **Default behavior is
154
+ unchanged** when no `--connector` is passed. Connectors are read-only re: the
155
+ CRM and run through the same evidence-gate/dedup/weight pipeline as `--from`
156
+ (the `readStagedSignals` row validator is now the shared `stagedRowToSignal`).
157
+ Library: `connectors/signalSources.ts` (`listSignalSources`, `getSignalSource`,
158
+ `fileSource`, `serpapiNewsSource`, `hubspotFormsSource`), `stagedRowToSignal` +
159
+ `StagedSignalRow` from `signals.ts`. Design:
160
+ [docs/spec-connectors-signals-outbound.md](../../docs/spec-connectors-signals-outbound.md)
161
+ (the connector taxonomy + the Phase 2/3 webhook-spool and governed-channel plan).
162
+ - **Webhook spool format + conventional landing zone (signals Phase 2).**
163
+ `--connector file` with no path now reads the conventional spool directory
164
+ (`<profile home>/signals/spool`, via `signalsSpoolDir`) — every `*.jsonl` in
165
+ it, name-sorted, so a webhook receiver can append one row per event (per
166
+ source: `rb2b.jsonl`, `hubspot.jsonl`, …) and they all land in one fetch. The
167
+ `file` connector reads a directory as well as a single file. New
168
+ [docs/signal-spool-format.md](./docs/signal-spool-format.md) documents the row
169
+ format, the landing zone, re-read/retention semantics, and per-platform
170
+ payload mappings (RB2B, Trigify, HubSpot form webhook). Per the open-core
171
+ boundary the format + reader are open; the always-on receiver is a hosted
172
+ concern — the package ships no receiver. Also fixes a latent filter bug:
173
+ explicitly-provided rows (`--from`, connectors) whose bucket has no configured
174
+ `sources` (notably `demand`) were silently dropped unless `--bucket` was given;
175
+ now only an explicit `--bucket` narrows them, so spool/`hubspot-forms` `demand`
176
+ signals flow by default.
177
+ - **Outbox channel — governed send terminus (signals Phase 3).** `apply
178
+ --channel outbox` renders each APPROVED drafted opener to a local outbox
179
+ (`<profile home>/signals/outbox/<channel>.jsonl`, via `signalsOutboxDir`) for a
180
+ downstream sender to drain — and **transmits nothing**, preserving the "drafts
181
+ everything, transmits nothing" invariant (the send-side mirror of the spool).
182
+ The outbox channel is a `GtmConnector` whose `applyOperation` renders instead
183
+ of writing a CRM, so it reuses the entire governed apply path (approval set,
184
+ integrity verification, idempotency, run recording) with no change to the apply
185
+ engine; `apply` now takes `--provider <crm>` OR `--channel <id>`. It only
186
+ renders `draft:`-policy `create_task` ops (any other op is skipped) and is
187
+ idempotent on the operation id. New
188
+ [docs/outbox-format.md](./docs/outbox-format.md). Per the open-core boundary the
189
+ governed artifact + format are open; the always-on sender that actually
190
+ transmits (ESP/LinkedIn) is a hosted concern — no sender ships in the package.
191
+ Library: `connectors/outboxChannel.ts` (`createOutboxChannelConnector`,
192
+ `createChannelConnector`, `listOutbox`, `OutboxEntry`), `signalsOutboxDir`.
193
+
194
+ ### Changed
195
+
196
+ - **`enrich acquire --save` is now schedulable** (for `tam populate`). It joins
197
+ the read/plan-side schedule allowlist in its plan-producing form ONLY — a
198
+ scheduled `enrich acquire` must include `--save` (rejected otherwise), so each
199
+ firing queues a `needs_approval` lead plan and writes nothing. The acquire
200
+ meter is still charged only at apply, and apply stays a separate human gate
201
+ (`apply --plan-id`, re-checked approved). Scheduling never auto-approves.
202
+
10
203
  ## [0.43.0] — 2026-06-25
11
204
 
12
205
  ### Added
package/README.md CHANGED
@@ -38,13 +38,17 @@ It installs a compact operating guide ([skills/fullstackgtm/SKILL.md](./skills/f
38
38
  ## Five-minute loop
39
39
 
40
40
  ```bash
41
- # 0. No credentials? Try it on a realistic, deliberately messy demo CRM
41
+ # 0. Scaffold a workspace: a starter icp.json, an enrich.config.json acquire
42
+ # preset, and a PLAYBOOK.md wired for your provider + discovery source
43
+ npx fullstackgtm init --provider hubspot --source pipe0
44
+
45
+ # 1. No credentials? Try it on a realistic, deliberately messy demo CRM
42
46
  npx fullstackgtm audit --demo
43
47
 
44
- # 1. Audit your real HubSpot portal (private app token or OAuth access token)
48
+ # 2. Audit your real HubSpot portal (private app token or OAuth access token)
45
49
  HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm audit --provider hubspot --out plan.json
46
50
 
47
- # 2. Review plan.json, then apply ONLY the operations you approve
51
+ # 3. Review plan.json, then apply ONLY the operations you approve
48
52
  HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm apply \
49
53
  --plan plan.json --provider hubspot \
50
54
  --approve op_abc123,op_def456 \
@@ -53,6 +57,8 @@ HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm apply \
53
57
 
54
58
  Nothing is ever written without an explicit `--approve`. Operations whose value is a human decision (`requires_human_*` placeholders, e.g. which owner to assign) are refused unless you supply a concrete `--value` override.
55
59
 
60
+ `init` keeps any files you already have (`--force` overwrites) and works with `--source pipe0|explorium|linkedin` and `--provider hubspot|salesforce`. The PLAYBOOK.md it writes wires the cold-start and outbound-loop recipes to your workspace; the full play set — cold start, trigger-based outbound, scheduling, ABM, hygiene-gating, TAM — is [docs/recipes.md](./docs/recipes.md).
61
+
56
62
  ## Call workflows: calls become governed evidence
57
63
 
58
64
  Calls are where pipeline truth lives. `call parse` normalizes any transcript dialect — `Speaker: text` lines (Fathom, Gong exports), `[Speaker]:` labels, or raw Granola utterance JSON — into canonical segments, insights, and `GtmEvidence` records.
@@ -211,8 +217,11 @@ LinkedIn is just another discovery source on the same scored → deduped → met
211
217
  Cleaning and filling the CRM tells you *who* to reach; it never tells you *when*. The **signal → judge → draft** loop adds timing — and, like everything else, it stays on the dry-run → approve → apply spine and sends nothing.
212
218
 
213
219
  ```bash
214
- # 1. Watch for movement. Free, no-auth public job boards in the box; funding/company/social via staged ingest.
220
+ # 1. Watch for movement. Free, no-auth public job boards in the box; pull from
221
+ # connected platforms via source connectors; webhook platforms via the spool.
215
222
  fullstackgtm signals fetch --bucket job --source greenhouse,lever,ashby --keywords "revops,growth" --save
223
+ fullstackgtm signals fetch --connector serpapi-news,hubspot-forms --save # news + first-party form demand
224
+ fullstackgtm signals fetch --connector file --save # webhook landing zone (see docs/signal-spool-format.md)
216
225
  fullstackgtm signals list --since 7d # ranked triggers, each with a verbatim source quote
217
226
 
218
227
  # 2. Decide who's worth a touch — and who isn't. Scores timing × fit × memory into send/nurture/skip.
@@ -220,8 +229,10 @@ fullstackgtm icp judge --signals-from latest --with-history --save
220
229
  fullstackgtm icp eval --golden default # gate: prove the judge is calibrated before any send (exits 2 if not)
221
230
 
222
231
  # 3. Draft the opener from the trigger. A create_task plan — proposed, never transmitted.
223
- fullstackgtm draft --from-judge latest --min-score 80 --save
224
- fullstackgtm plans approve <id> --operations all && fullstackgtm apply --plan-id <id> --provider hubspot
232
+ fullstackgtm draft --from-judge latest --min-score 80 --channel email --save
233
+ fullstackgtm plans approve <id> --operations all
234
+ fullstackgtm apply --plan-id <id> --provider hubspot # log the touch as a CRM task
235
+ fullstackgtm apply --plan-id <id> --channel outbox # OR render to the outbox for a sender — transmits nothing (docs/outbox-format.md)
225
236
 
226
237
  # 4. Close the loop. Outcomes re-weight which signals earn a touch.
227
238
  fullstackgtm signals outcome --account acme.com --result replied
@@ -289,7 +300,7 @@ fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
289
300
  - `--demo` (with `--seed`) generates a realistic mid-market CRM with injected real-world failure modes — departed owners, unlinked deals, orphan accounts, stale pipeline — so agents and CI can exercise the full snapshot → audit → apply pipeline with zero credentials.
290
301
  - Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
291
302
 
292
- "Built for agents" is measured, not asserted: a 1,088-run benchmark (17 scenarios = 14 synthetic + 3 seeded from an anonymized real portal, × 3 tool-surface arms × 4 trials, across six models from three vendors, deterministic graders over final CRM state, τ-bench-style pass^k) shows the gated CLI surface beating raw CRM-API access on completion-under-policy for every model tested — and the tool-surface effect is monotonic and vendor-independent. Full matrix and methodology: [the leaderboard](./evals/crm/leaderboard/RESULTS.md).
303
+ "Built for agents" is measured, not asserted: a 1,892-run benchmark (20 scenarios = 17 synthetic + 3 seeded from an anonymized real portal, × 3 tool-surface arms × up to 4 trials, across nine models from six vendors, deterministic graders over final CRM state, τ-bench-style pass^k) shows the gated CLI surface beating raw CRM-API access on completion-under-policy for every model tested — and the tool-surface effect is monotonic and vendor-independent. Full matrix and methodology: [the leaderboard](./evals/crm/leaderboard/RESULTS.md).
293
304
 
294
305
  The design is **deterministic apply, governed suggest**: the parts that touch your CRM — the audit rules, the plan/apply contract, compare-and-set, the survivor/merge logic — are deterministic and replayable; the parts that read free text (`call parse`/`score`, `market classify`) are LLM-powered but bounded, with every quoted span mechanically verified against the source before it can drive a writeback. Nondeterministic suggestion, deterministic governance.
295
306