fullstackgtm 0.57.0 → 0.58.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/README.md CHANGED
@@ -2,15 +2,23 @@
2
2
 
3
3
  [![CI](https://github.com/fullstackgtm/core/actions/workflows/ci.yml/badge.svg)](https://github.com/fullstackgtm/core/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/fullstackgtm)](https://www.npmjs.com/package/fullstackgtm) [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
4
4
 
5
- **Guardrails for AI agents working in your CRM.**
5
+ **The open-source control plane and safety toolbox for AI agents working in your CRM.**
6
6
 
7
- Let Claude or Codex loose on Salesforce or HubSpot and it will make predictable mistakes duplicates, bad batch edits, silent overwrites. fullstackgtm is the wrapper that knows those mistakes: every change becomes a reviewable plan before anything is written.
7
+ An agent can read HubSpot or Salesforce in seconds. The dangerous part is what happens next: duplicate creation, stale-data overwrites, unbounded batch edits, guessed owners, and writes nobody can reconstruct later.
8
8
 
9
- Instead of overwriting records, an agent using fullstackgtm sets out a **patch plan**: what it wants to change, field by field, with before/after values and the reason. You review it, roll it out incrementally (approve 10 operations, then 100, then all), and every write records the before value — so a mistake is visible and reversible, not silently absorbed. Think `terraform plan` for your CRM.
9
+ fullstackgtm puts one governed contract between the agent and the CRM:
10
10
 
11
- **What it catches:** duplicates, orphaned records, bad batch edits, stale deals, missing owners — deterministic rules, not LLM guesses. Works as a CLI, a library, or an MCP server your agent calls directly.
11
+ ```text
12
+ source data → canonical snapshot → deterministic checks → patch plan → human approval → guarded apply → receipt
13
+ ```
14
+
15
+ Every proposed change carries its before/after value, reason, risk, and stable operation ID. Nothing is written until specific operations are approved. At apply time, the connector re-checks identity, preconditions, and current provider state rather than trusting an old plan.
16
+
17
+ Think **`terraform plan` for your CRM**.
12
18
 
13
- Open source (Apache-2.0), zero runtime dependencies. Beta (0.x): APIs may shift before 1.0; the safety invariants never do. Connectors: HubSpot, Salesforce (read/write), Stripe (read-only).
19
+ fullstackgtm is not another B2B database, autonomous SDR, or enrichment vendor. It is the provider-neutral control layer around those systems. HubSpot and Salesforce are the first write targets; Stripe, Apollo, Clay, ZoomInfo, Pipe0, Explorium, HeyReach, and other systems can supply evidence or source data without bypassing the same safety gate.
20
+
21
+ Open source under Apache-2.0. Beta (0.x): APIs may shift before 1.0; the safety invariants do not.
14
22
 
15
23
  ## Install
16
24
 
@@ -21,530 +29,229 @@ npm install github:fullstackgtm/core # or straight from this repo (projec
21
29
  npx github:fullstackgtm/core audit --demo # zero-install from the repo
22
30
  ```
23
31
 
24
- (Global `npm install -g` from a git URL is unreliable on npm 11 — it symlinks into npm's temp cache. Use the registry for global installs, or the project-local/npx forms above.)
25
-
26
- Requires Node 20+. The core has zero runtime dependencies; only the MCP server entrypoint uses the optional peers `@modelcontextprotocol/sdk` and `zod`.
32
+ Requires Node 20+. The core has zero runtime dependencies. The MCP entrypoint uses the optional peers `@modelcontextprotocol/sdk` and `zod`.
27
33
 
28
34
  ```bash
29
- npx fullstackgtm doctor # verify the install: node version, credentials, MCP peers, next step
35
+ npx fullstackgtm doctor
30
36
  ```
31
37
 
32
- Installing for an AI agent? The fastest path is the agent skill:
38
+ Installing for an agent:
33
39
 
34
40
  ```bash
35
- npx skills add fullstackgtm/core # Claude Code, Cursor, Codex, and other skills-compatible agents
41
+ npx skills add fullstackgtm/core
36
42
  ```
37
43
 
38
- It installs a compact operating guide ([skills/fullstackgtm/SKILL.md](./skills/fullstackgtm/SKILL.md)) — the governed loop, the safety invariants, and the verb map — so the agent reaches for plans instead of raw writes. For a deterministic install-and-verify script with expected outputs, hand it [INSTALL_FOR_AGENTS.md](./INSTALL_FOR_AGENTS.md). A documentation map lives in [llms.txt](./llms.txt).
44
+ See [INSTALL_FOR_AGENTS.md](./INSTALL_FOR_AGENTS.md) for deterministic install verification and [llms.txt](./llms.txt) for the machine-readable documentation map.
39
45
 
40
- ## Five-minute loop
46
+ ## The governed CRM loop
41
47
 
42
48
  ```bash
43
- # 0. Scaffold a workspace: a starter icp.json, an enrich.config.json acquire
44
- # preset, and a PLAYBOOK.md wired for your provider + discovery source
45
- npx fullstackgtm init --provider hubspot --source pipe0
49
+ # 1. Read the CRM and produce a plan. This never writes.
50
+ HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm audit \
51
+ --provider hubspot --out plan.json
46
52
 
47
- # 1. No credentials? Try it on a realistic, deliberately messy demo CRM
48
- npx fullstackgtm audit --demo
53
+ # 2. Review findings and proposed operations.
54
+ npx fullstackgtm plans show <plan-id>
49
55
 
50
- # 2. Audit your real HubSpot portal (private app token or OAuth access token)
51
- HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm audit --provider hubspot --out plan.json
56
+ # 3. Approve only the operations you intend to authorize.
57
+ npx fullstackgtm plans approve <plan-id> --operations op_abc123,op_def456
52
58
 
53
- # 3. Review plan.json, then apply ONLY the operations you approve
59
+ # 4. Apply the approved subset. Current provider state is checked again.
54
60
  HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm apply \
55
- --plan plan.json --provider hubspot \
56
- --approve op_abc123,op_def456 \
57
- --value op_def456=2026-09-30
58
- ```
59
-
60
- 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.
61
-
62
- `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).
63
-
64
- ## Call workflows: calls become governed evidence
61
+ --plan-id <plan-id> --provider hubspot
65
62
 
66
- 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.
67
-
68
- **Extraction is LLM-powered by default, with your own key.** The first time you run `call parse` or `call score`, the CLI asks for an Anthropic or OpenAI API key (auto-detected from the prefix), validates it against the provider, and stores it in the 0600 credential store — same treatment as CRM logins. Or skip the prompt: set `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`, or `echo "$KEY" | fullstackgtm login anthropic` (or `openai`). The key talks directly to your provider — raw fetch, no SDK, no middleman. `--model` overrides the defaults (claude-haiku-4-5 / gpt-4o-mini). LLM insights carry verbatim-quote evidence and, for next steps, owner/deadline/commitment. Pass `--deterministic` for the free, instant, byte-stable keyword baseline (no key needed — right for CI and warehouse bulk loads). Every insight is provenance-marked (`extractor: "llm:anthropic:…"` vs `"deterministic"`).
69
-
70
- **`call classify`** picks the call type before scoring, because a renewal scored on "Depth of Discovery" is just wrong. It runs a **deterministic, key-free** signal classifier over the transcript — 12 call types (prospecting, discovery, demo, technical_validation, negotiation, closing, onboarding, check_in, renewal_expansion, qbr, internal, other) — returning the type with a confidence and a written reason; pass `--llm` for a model tiebreak on the ambiguous middle. It needs no API key, so it runs free in CI and on warehouse bulk loads (`--list` prints the taxonomy).
71
-
72
- **`call score`** rates the call against a coaching rubric. By default it **auto-selects the type-specific rubric** from the classification — each call type ships its own dimensions with anchored high/low examples and evidence cues (cuts scoring variance). Override with `--call-type <type>` to force a preset, or `--rubric rubric.json` for your own (`{ scale, name?, callType?, bands?, dimensions: [{ name, weight, rubric, anchorsHigh?, anchorsLow?, evidenceCues?, coachingPrompts? }] }`). The weighted overall is computed deterministically client-side, mapped to a qualitative band, every dimension score is evidence-quoted, and the rubric file is where your client-specific coaching framework lives (`--list-rubrics` prints the built-in presets).
73
-
74
- `call link` answers "which deal was this call about" from attendee domains (account domain or contact emails → open deals, most recent activity first, with confidence + reason). `call plan` turns next-step insights into the same governed plan lifecycle as everything else.
75
-
76
- ```bash
77
- # Coaching pipeline (Slack + CRM): parse → classify → score → link → govern the writeback
78
- fullstackgtm call parse --transcript call.txt --title "Acme disco" --out parsed.json # LLM extraction
79
- fullstackgtm call classify --call parsed.json # deterministic call type
80
- fullstackgtm call score --call parsed.json # auto-selects the type's rubric
81
- fullstackgtm call score --call parsed.json --rubric team-rubric.json # …or bring your own framework
82
- fullstackgtm call link --attendees jane@acme.com --provider hubspot # → deal id + reason
83
- fullstackgtm call plan --call parsed.json --deal 123 --provider hubspot --save
84
- # review → plans approve → apply: deal.next_step + follow-up tasks, compare-and-set protected
85
- # (pipe the parse/score JSON into Slack/Notion however you like)
86
-
87
- # Analytics pipeline (warehouse): one flat NDJSON row per insight
88
- for t in transcripts/*; do fullstackgtm call parse --transcript "$t" --ndjson --deterministic; done > insights.ndjson
89
- # free keyword baseline for bulk loads; drop --deterministic for LLM-quality rows
90
- # COPY into your warehouse (stable call/evidence ids make reloads idempotent)
63
+ # 5. Verify the signed execution record.
64
+ npx fullstackgtm audit-log verify
91
65
  ```
92
66
 
93
- The boundary that remains: Slack/Notion/warehouse sinks are *your* pipeline, composed around the JSON and your rubrics and keys stay yours.
67
+ For an unsaved file plan, `apply --plan plan.json --approve <ids>` provides the same explicit operation selection. A `requires_human_*` placeholder is never executable until a concrete value is supplied.
94
68
 
95
- ## The create gate: no new dupes
69
+ ## The safety contract
96
70
 
97
- Detection cleans up yesterday's duplicates; the **resolve gate** prevents tomorrow's. Before any writer — a sync job, a webhook handler, an agent, your own script — creates a record, ask the gate:
71
+ These rules apply across the CLI, library, scheduler, and MCP server:
98
72
 
99
- ```bash
100
- fullstackgtm resolve contact --email jane@acme.com --input snap.json # exit 0 = safe to create
101
- fullstackgtm resolve account --domain acme.com --provider hubspot # exit 2 = exists/ambiguous: do NOT create
102
- fullstackgtm resolve deal --name "Acme Expansion" --account-id 123 --input snap.json
103
- ```
73
+ 1. **Read and plan by default.** Audits and previews do not mutate providers.
74
+ 2. **Explicit authority.** Apply writes only approved operation IDs; approval is never inferred from a prompt.
75
+ 3. **No guessed decisions.** Human placeholders require concrete value overrides.
76
+ 4. **Resolve before create.** Domain, email, and open-deal identity gates stop duplicate creation.
77
+ 5. **Compare before set.** Before-values and preconditions prevent silent stale-plan overwrites.
78
+ 6. **Bound the blast radius.** Bulk operations support guards, filters, and operation caps.
79
+ 7. **Record the outcome.** Every attempt returns applied, skipped, or failed receipts; audit logs are tamper-evident.
80
+ 8. **Separate clients.** Profiles scope credentials, plans, enrichment state, and schedules to one organization.
81
+ 9. **Never auto-approve.** Scheduled work may generate plans; it cannot grant itself authority to execute them.
104
82
 
105
- Identity keys match the audit/merge engines exactly: account domain (normalized), contact email, and the open-deal key (account + normalized name). Names alone are never identity — they return `ambiguous` with the candidates, not a guess. Exit codes are gate-shaped for scripts: `0` safe to create, `2` match found, `1` error. For high-volume writers, pair it with a cron-refreshed snapshot file rather than a live `--provider` fetch per call. Also exposed as `resolveRecord()` and the MCP tool `fullstackgtm_resolve`.
83
+ ## CRM safety toolbox
106
84
 
107
- **Provenance attribution** closes the loop on recurring dupes: snapshots now capture each record's source (HubSpot's read-only `hs_object_source*` fields), and duplicate findings name the writer — `"3 accounts share acme.com … Created by: Gojiberry (app-123) ×2, CRM_UI"` — so you fix the integration, not just the records. Records created by this CLI stamp their own provenance (`hs_object_source_detail_2`, best-effort).
108
-
109
- ## From findings to fixes: the suggest chain
110
-
111
- Most placeholder answers are already derivable from your own CRM data. `suggest` computes them deterministically — account-name matching cross-checked against contact associations — with a confidence level and a written reason per operation, so you (or an agent) approve evidence, not guesses:
85
+ ### Prevent
112
86
 
113
87
  ```bash
114
- fullstackgtm audit --provider hubspot --save # Saved plan patch_plan_abc123
115
- fullstackgtm suggest --plan-id patch_plan_abc123 --provider hubspot --out suggestions.json
116
- # review suggestions.json: every value carries confidence (high/low/create/none) + a reason
117
- fullstackgtm plans approve patch_plan_abc123 --values-from suggestions.json # high-confidence only by default
118
- fullstackgtm apply --plan-id patch_plan_abc123 --provider hubspot
88
+ # Exit 0: safe to create. Exit 2: exists or ambiguous; do not create.
89
+ fullstackgtm resolve contact --email jane@acme.com --provider hubspot
90
+ fullstackgtm resolve account --domain acme.com --provider salesforce
119
91
  ```
120
92
 
121
- Widen the bar deliberately: `--min-confidence low` accepts single-signal matches and **merge survivor suggestions** (irreversible merges are capped at low confidence by design, so the default bar never bulk-approves one); `--include-creates` accepts `create:<Name>` values approving one **creates the missing company/account record and links to it** in a single audited operation, so even record creation stays inside the typed, human-approved model. Conflicting or ambiguous evidence always yields *no* suggestion with an explanation, never a guess.
93
+ Identity is based on normalized account domains, contact emails, and account + deal-name keys. Names alone are not identity. When the provider exposes record provenance, duplicate findings identify the writer that created them.
94
+
95
+ ### Detect
122
96
 
123
97
  ```bash
124
- # 3. Hand the findings to whoever owns the CRM: a client-ready report
125
- npx fullstackgtm report --provider hubspot --client "Acme" --out acme-health.html
98
+ fullstackgtm snapshot --provider hubspot --out snapshot.json
99
+ fullstackgtm audit --input snapshot.json --json
100
+ fullstackgtm audit --provider hubspot --fail-on warning
101
+ fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
102
+ fullstackgtm health
126
103
  ```
127
104
 
128
- `report` renders the same audit as a deliverable severity counts up front, a prose summary, per-rule detail with example records, and next steps as markdown or self-contained HTML (printable, emailable, no external assets).
105
+ Built-in deterministic rules cover duplicates, orphaned records, missing owners, unlinked or amount-less deals, stale pipeline, past close dates, and related hygiene failures. Stable finding and operation IDs make repeated runs diffable.
129
106
 
130
- ## Routine maintenance as governed verbs: bulk-update, dedupe, reassign, fix
107
+ ### Remediate
131
108
 
132
- The maintenance work RevOps actually does in bulk — backfills, book transfers, duplicate sweeps, "just fix everything that rule found" — gets first-class verbs. Each one *builds a plan*; nothing executes without the same approve → apply gauntlet as everything else.
109
+ Every write-shaped command below produces a patch plan before it can execute:
133
110
 
134
111
  ```bash
112
+ fullstackgtm fix --rule missing-deal-owner --provider hubspot
135
113
  fullstackgtm bulk-update deal --where "stage=closedwon" --where "amount:empty" \
136
- --set amount=from:account.annualrevenue --save # per-record derived values; empty sources skipped, never guessed
137
- fullstackgtm dedupe account --key domain --keep richest --save # one merge_records op per duplicate group
138
- fullstackgtm reassign --from 411 --to 902 --except-deal-stage closing --save # ownership handoff playbook
139
- fullstackgtm reassign --assign-unowned --to 902 --save # claim every ownerless record for an owner
140
- fullstackgtm fix --rule missing-deal-owner --provider hubspot --yes # audit one rule → suggest → approve → apply, one command
141
- fullstackgtm backfill stripe --save # paid Stripe invoices → proposed closed-won deals, deduped by invoice id
114
+ --set amount=from:account.annualrevenue --save
115
+ fullstackgtm dedupe account --key domain --keep richest --save
116
+ fullstackgtm reassign --from 411 --to 902 --except-deal-stage closing --save
117
+ fullstackgtm route --provider hubspot --save
118
+ fullstackgtm backfill stripe --save
142
119
  ```
143
120
 
144
- `backfill stripe` makes the CRM honest when Stripe is the revenue source of truth: one closed-won deal per paid invoice (amount = invoice total, close date = paid date, company association by billing-email domain). A customer the CRM doesn't know gets a proposed account create in the same approval-gated plan (freemail billing domains are never used as a company domain; `--skip-unmatched` keeps them report-only), and apply re-resolves each invoice id so re-running never double-creates.
145
-
146
- `bulk-update` filters the snapshot (`=`, `!=`, `~` substring, `!~` not-substring, `:empty`/`:notempty`, type-aware comparisons `<` `>` `<=` `>=` where `today` resolves to the policy date — e.g. `closeDate<today` — and date/numeric fields coerce by value form, `|` any-of, relational pseudo-fields like `account.domain` or `openDealStages`) into a dry-run patch plan — and **the full filter is re-verified per record at apply time**, with mid-apply rechecks, so a record that stopped matching between audit and apply is skipped, not clobbered. For date/count hygiene (past close dates, stale deals, missing accounts, duplicates), prefer the rule-backed `fix --rule <id>` — the rule encodes the open-deal + date logic deterministically; use `bulk-update` only when no rule covers the task. Equality filters double as preconditions; `--require` adds explicit ones; `--guard` asserts cross-record conditions; `--max-operations` caps blast radius. `--set field=from:<sourceField>` derives values per record; `--create-task <text>` is the third change mode, emitting approval-gated `create_task` operations instead of field writes; `--archive` refuses records whose identity key (account domain, contact email) is shared with another record — that's a duplicate, and duplicates are merged with `dedupe`, not archived around (`--force-archive-duplicates` overrides that refusal explicitly).
121
+ Irreversible merges remain low-confidence-capped. Filters and guards are re-evaluated against live state during apply, so a record that stopped matching is skipped rather than clobbered.
147
122
 
148
- `dedupe` finds duplicate groups by normalized identity key and emits one `merge_records` operation per group with a deterministic survivor (`richest` = most populated fields, ties to lowest id; `oldest`). Merges stay irreversible-and-therefore-low-confidence-capped on approval, exactly like merge suggestions from the audit. `reassign` is the ownership-handoff playbook: one plan per object type, extra scoping account-lifted to deals and contacts, and `--except-deal-stage` excludes both deals in that stage and every record whose account has an open deal in it. `fix` is the one-shot composite for a single rule: audit → save → suggest → approve suggestion-backed operations at the confidence bar → with `--yes`, apply and print the stage-by-stage summary; without it, stop after approval and print the apply command.
123
+ ### Govern third-party enrichment
149
124
 
150
- ## The market map: the category, observed
151
-
152
- Your CRM records what happened in your own deals; nothing records the shape of the category you sell into. The **market map** does: vendors and a claim taxonomy live in a reviewable `market.config.json`, vendor pages are captured into a content-addressed cache, every vendor × claim cell gets a messaging-intensity reading (LOUD / QUIET / ABSENT — with UNOBSERVABLE for failed captures, never a fake absence), and deterministic front states fall out per claim: open, contested, owned, saturated.
125
+ Enrichment data never writes directly to the CRM. A source record is matched deterministically, mapped under a declared policy, and rendered as a reviewable diff. The current conflict policy is deliberately conservative: fill blanks, and refresh only fields the enrichment ledger proves it previously stamped.
153
126
 
154
127
  ```bash
155
- fullstackgtm market init --category creative-intelligence # seed vendors + claims, edit by hand
156
- fullstackgtm market capture # fetch pages content-addressed captures
157
- fullstackgtm market classify # LLM readings (BYO key), every quote verified
158
- fullstackgtm market fronts --diff run-1 # what changed since last run
159
- fullstackgtm market report --format html --out map.html # the client-ready field report
160
- fullstackgtm market refresh # all of the above, weekly, one command
161
- ```
128
+ # Apollo pull
129
+ fullstackgtm enrich append --source apollo --provider hubspot --save
162
130
 
163
- The discipline matches the rest of the tool. Intensity readings are *proposals* — from the LLM (`classify`, same bring-your-own-key seam as `call parse`, provenance-marked) or from any agent/human (`market worksheet` → `market observe`) — and **every quoted evidence span is verified character-for-character against the stored capture it cites** before an observation is accepted. Quotes that aren't on the page bounce. Everything downstream of the store is deterministic: same observations, same map.
131
+ # Clay export / push-style source
132
+ fullstackgtm enrich ingest clay.csv --source clay --provider hubspot --save
164
133
 
165
- `market axes` is for earning a strategic 2×2 instead of asserting one: PCA over the intensity matrix (PC1 = the category's own primary axis, PC2 = the most differentiating direction orthogonal to it), triangulation of your configured axes against the data, and an orthogonality screen that flags two axes that are secretly one. Axes are claim-scoring rubrics in the config; the report renders the primary pair as the strategic map. Captures and observations are profile-scoped (`~/.fullstackgtm/market/<category>`), so one client's category intel never bleeds into another's.
166
-
167
- Two more derivations close the loop from map to action. `market overlay --snapshot <crm.json> [--calls <files>]` joins the observation store against your own ground truth — which claims and vendors actually come up in your deals and call transcripts (deterministic word-boundary matching, no LLM) — and emits OCCUPY / PROMOTE / URGENT / RETREAT directives, each carrying at least one observation and one CRM stat with its sample size; below the evidence thresholds the honest answer is *no directive*. `--save` turns directives into approval-gated `create_task` operations through the normal plan chain. `market scale` estimates each vendor's size from **citable** signals you record in the config (G2 review counts, LinkedIn headcount, revenue claims — each with source URL, verbatim quote, and caveat): every signal is converted into revenue space first, calibrated within the vendor set and stratified by ACV band, then combined as a weighted geometric mean with the uncertainty spread and calibration table disclosed. The report's strategic-map bubbles become area-proportional to estimated revenue share — captioned "citable but NOT audited" — and without signals, dots are uniform and the caption says size carries no meaning.
168
-
169
- ## Governed enrichment: a diff you approve before third-party data touches your CRM
170
-
171
- Every enrichment vendor ships fire-and-forget writeback. The **enrich layer** inverts that: declare once which fields come from which source under which conflict policy (`enrich.config.json` — sources, ordered match keys, field mappings, policy), then `enrich append` fills the gaps and `enrich refresh` keeps them current — with every write passing through the normal dry-run → approval → apply contract, and every value traceable to the source payload that produced it.
172
-
173
- ```bash
174
- echo "$APOLLO_API_KEY" | fullstackgtm login apollo # BYO key, stored 0600
175
- fullstackgtm enrich append --provider hubspot # pull → match → dry-run diff, writes NOTHING
176
- fullstackgtm enrich append --provider hubspot --save # persist the plan (needs_approval) + run record
177
- fullstackgtm enrich ingest clay-export.csv --source clay # stage a push-style source (Clay CSV / webhook JSON)
178
- fullstackgtm enrich refresh --source apollo --save # re-check stale stamped fields; ops only where the source changed
179
- fullstackgtm enrich status --runs # last run per source, counts, staleness, interrupted-run cursor
134
+ # Inspect source runs, stamps, and stale fields
135
+ fullstackgtm enrich status --runs
180
136
  ```
181
137
 
182
- Matching is deterministic: ordered keys, unique hit wins, zero hits falls through to the next key, and multiple hits are never guessed away — they skip (recorded with candidate ids) or flow into the existing `suggest` → `plans approve` chain as `requires_human_record_selection` placeholders. The MVP conflict policy is `never`: enrich only fills blank fields, and `refresh` only re-touches fields its own run-store ledger proves it stamped (per-record/per-field `enrichedAt`, profile-scoped, never written into your portal as custom properties). The `system-only` and `always` rungs of the ladder are phase 2 and are refused explicitly, not silently accepted. Recurring execution belongs to the scheduler — enrich owns no cron logic.
183
-
184
- ## Acquire: net-new leads, ICP-targeted and dupe-safe by default
138
+ #### ZoomInfo as a governed source
185
139
 
186
- `enrich append/refresh` fill blanks on records you already have. **`enrich acquire`** creates *net-new* ones and routes them through the same dry-run approve → apply gate, so prospecting can never silently write to your CRM. It discovers people, resolves their work email, scores them against your ICP, dedupes against your CRM, and proposes governed `create_record` operations.
140
+ fullstackgtm uses ZoomInfo's official [`gtm` CLI](https://github.com/Zoominfo/gtm-ai-cli) for read-only retrieval. ZoomInfo keeps ownership of OAuth, entitlements, and credit accounting; fullstackgtm keeps ownership of matching, policy, approval, CRM apply, and receipts. No ZoomInfo token is copied into the fullstackgtm credential store.
187
141
 
188
142
  ```bash
189
- fullstackgtm icp interview # emit the ICP question spec; an agent asks it (AskUserQuestion)
190
- fullstackgtm icp set answers.json --name "RevOps ICP" # write icp.json (firmographics + persona + fit threshold)
191
- echo "$PIPE0_API_KEY" | fullstackgtm login pipe0 # discovery + work-email provider, stored 0600
192
-
193
- fullstackgtm enrich acquire --provider hubspot # zero-config: ICP → discover → score → dedup → dry-run diff
194
- fullstackgtm enrich acquire --provider hubspot --max 25 --save # cap the batch, persist the needs_approval plan
195
- fullstackgtm enrich acquire --provider hubspot --max 25 --scan-limit 500 --save # scan through duplicate-heavy pages for 25 fresh leads
196
- fullstackgtm plans approve <id> --operations all
197
- fullstackgtm apply --plan-id <id> --provider hubspot # the only step that creates contacts
143
+ brew install zoominfo/gtm-ai/gtm-ai-cli # or: npm install -g @zoominfo/gtm-ai-cli
144
+ gtm auth login
198
145
  ```
199
146
 
200
- The **ICP** (`icp.json`) is the single targeting artifact: it generates each provider's discovery filters (Explorium, pipe0/Crustdata) *and* fit-scores every discovered prospect — only above-threshold leads are proposed. Develop one by interview; the CLI can't run `AskUserQuestion` itself, so `icp interview` emits the spec and an agent (Claude Code / Codex) drives it.
147
+ Declare ZoomInfo paths in `enrich.config.json` (the raw JSON record is retained as operation evidence):
201
148
 
202
- When the CLI is paired, the hosted workspace and local ICP can be edited
203
- without last-write-wins data loss:
204
-
205
- ```bash
206
- fullstackgtm icp status --domain acme.com --icp ./icp.json
207
- fullstackgtm icp push --domain acme.com --icp ./icp.json --change-summary "Narrowed buyer titles"
208
- fullstackgtm icp sync --domain acme.com --icp ./icp.json
209
- fullstackgtm icp pull --domain acme.com --out ./icp.json --force
149
+ ```json
150
+ {
151
+ "sources": { "zoominfo": { "kind": "api" } },
152
+ "match": {
153
+ "company": { "keys": ["domain", "name"], "onAmbiguous": "skip" },
154
+ "contact": { "keys": ["email", "name"], "onAmbiguous": "skip" }
155
+ },
156
+ "fields": {
157
+ "company": [
158
+ { "crm": "employeeCount", "from": { "zoominfo": "attributes.employeeCount" }, "refresh": true },
159
+ { "crm": "annualRevenue", "from": { "zoominfo": "attributes.revenue" }, "refresh": true }
160
+ ],
161
+ "contact": [
162
+ { "crm": "title", "from": { "zoominfo": "attributes.jobTitle" }, "refresh": true },
163
+ { "crm": "phone", "from": { "zoominfo": "attributes.phone" }, "refresh": true }
164
+ ]
165
+ },
166
+ "policy": { "overwrite": "never", "defaultStaleDays": 90 }
167
+ }
210
168
  ```
211
169
 
212
- Published hosted edits are immutable numbered revisions. The CLI stores only
213
- sync metadata in `./.fullstackgtm/icp.json.sync.json`; the portable `icp.json`
214
- schema is unchanged. `sync` never silently replaces the local file: if hosted
215
- changed, it writes `icp.json.hosted-rN.json` for review. If both sides changed,
216
- it reports a conflict rather than unioning ordered targeting arrays. Signal and
217
- lead-preview records retain the exact ICP revision used for later analysis.
218
-
219
- **You don't pay to re-discover dupes.** Before the (credit-spending) email step, acquire drops prospects already in your CRM and any seen in a prior run:
220
-
221
- - **Pre-email CRM dedup** matches the live snapshot. The LinkedIn URL (`hs_linkedin_url`, read into the snapshot by default — safe everywhere, HubSpot ignores unknown properties) is the strong key; name+domain is the fallback. Created contacts get their LinkedIn URL written back, so coverage — and dedup precision — grows over time. If your CRM has no LinkedIn URLs, acquire says so and recommends populating it.
222
- - **A cross-run seen cache** remembers everyone already resolved, so re-running the same ICP costs nothing for known people.
223
- - Two more checkpoints stay precise: the email-level match at plan build, and **resolve-first at apply** (re-checks the live CRM, never double-creates, never charges the meter).
224
-
225
- Every run is **metered**: a per-profile budget caps both record count and provider spend (per-day + per-month, whichever binds first), charged only for creates that actually land. `enrich acquire` runs with just an `icp.json` and a `login` — sensible defaults for budget, provider, and create-mapping ship in the box.
226
-
227
- Discovery continuation is profile-scoped and independently keyed by the exact
228
- provider, source, list, and ICP query. Scheduled runs resume the saved Pipe0
229
- cursor or HeyReach offset instead of rereading page one, even when several
230
- lists alternate. When the CLI is paired, this non-PII checkpoint is mirrored
231
- to the hosted organization with compare-and-swap so another authenticated
232
- worker can resume without moving a newer cursor backwards. `--max` is the desired net-new plan size;
233
- `--scan-limit` independently bounds raw candidates scanned after ICP filtering
234
- and dedupe. `enrich status --runs` exposes the full funnel and whether the
235
- audience is continuing or provider-confirmed exhausted.
236
-
237
- When paired, every saved plan is also mirrored into the hosted Patch Plans
238
- review queue and the CLI prints its exact deep-link. The mirror is immutable
239
- and organization-scoped: replaying the same plan is idempotent, while the same
240
- id with different content is rejected. Hosted reviewers can approve or reject;
241
- the plan is executable from any synchronized surface with a compatible CRM
242
- connection. A CLI-created plan can be applied in hosted, and a hosted approval
243
- can be applied from the CLI—the plan's origin does not permanently own it.
244
-
245
- This is local-first replication, not remote control. The local plan store works
246
- offline, and plan commands perform a best-effort hosted check-in. Run
247
- `fullstackgtm plans sync` to explicitly exchange every plan's latest approval,
248
- execution claim, status, and receipts. If hosted applies while the CLI is
249
- offline, the next check-in imports the exact per-operation results and marks
250
- the local plan applied. If the CLI applies while hosted is unavailable, it
251
- keeps the result locally and uploads the receipt on a later check-in.
252
-
253
- Before either surface writes, it verifies the immutable plan hash, selected
254
- operation ids, current approval revision, connector capability, and provider
255
- state. Online replicas use a short-lived execution claim to coordinate; stable
256
- operation ids, resolve-before-create, compare-and-set guards, and provider
257
- readback protect offline retries. Receipts record each operation as `applied`,
258
- `skipped`, or `failed`, plus provider record ids when available, so another
259
- replica learns what happened rather than inferring it from plan status. A
260
- conflicting plan body or approval revision is surfaced for review and is never
261
- merged by expanding authority.
262
-
263
- Interactive command output is decision-first by default: compact cards show
264
- status, selected scope, expected effect, per-record outcomes, hosted links, and
265
- the next safe command. Use `--verbose` on supported human-facing commands for
266
- the complete findings/evidence/payload document; use `--json` for the stable
267
- machine-readable contract. Progress and warnings remain on stderr, while JSON
268
- and requested report data remain clean on stdout.
269
-
270
- Local signing keys and HMAC digests never upload. Operation before/after values
271
- do upload to the paired organization because they are required for informed
272
- review and hosted execution.
273
-
274
- **Discovery sources** are zero-config presets behind `--source`: `explorium` and `pipe0` run ICP-driven people search, while `linkedin` (Phase 1 — discovery + dry-run) reads a pre-built **HeyReach** lead list:
170
+ Then preview or save the plan:
275
171
 
276
172
  ```bash
277
- echo "$HEYREACH_API_KEY" | fullstackgtm login heyreach # or set HEYREACH_API_KEY
278
- fullstackgtm enrich acquire --source linkedin --list <listId> --provider hubspot # score → dedup → dry-run diff
173
+ fullstackgtm enrich append --source zoominfo --provider hubspot
174
+ fullstackgtm enrich append --source zoominfo --provider hubspot --save
175
+ fullstackgtm enrich refresh --source zoominfo --provider hubspot --save
279
176
  ```
280
177
 
281
- LinkedIn is just another discovery source on the same scored deduped metered dry-run→approve→apply spine; the LinkedIn profile URL is the match key and the list costs `$0`/record. **It never sends** connect/message operations are out of scope for Phase 1, so without `--save` there is zero send/ToS exposure.
282
-
283
- **Leads are never born ownerless.** An `acquire.assign` policy stamps an owner onto every created lead at create time (mapped to HubSpot `hubspot_owner_id`), routed by one of four strategies — `fixed`, `round-robin` (distributed deterministically, no rotation state to drift), `territory` (by geo / industry / size / department / title), or `account-owner` (inherit the matched company's owner). With no policy and a single-owner portal, acquire defaults every lead to that owner (or pass `--assign-owner <id>`); with multiple owners and no policy it warns and leaves them unassigned rather than guess. To clear *existing* ownerless records, `reassign --assign-unowned --to <ownerId>` applies the same intent as a backfill.
284
-
285
- ## Signal-based outbound: reach accounts the week something changes
178
+ `ZOOMINFO_GTM_BIN` can point to a non-default `gtm` executable. Source retrieval can consume ZoomInfo bulk data credits even when the resulting FullstackGTM plan is only a dry run; use ZoomInfo admin credit limits and start with a small CRM snapshot. The CRM still receives no write until the plan is approved and applied.
286
179
 
287
- 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.
180
+ ### Verify and recover
288
181
 
289
182
  ```bash
290
- # 1. Watch for movement. Free, no-auth public job boards in the box; pull from
291
- # connected platforms via source connectors; webhook platforms via the spool.
292
- fullstackgtm signals fetch --bucket job --source greenhouse,lever,ashby --keywords "revops,growth" --save
293
- fullstackgtm signals fetch --connector serpapi-news,hubspot-forms --save # news + first-party form demand
294
- fullstackgtm signals fetch --connector file --save # webhook landing zone (see docs/signal-spool-format.md)
295
- echo "$EXA_API_KEY" | fullstackgtm login exa # once; or keep the key in env
296
- fullstackgtm signals discover --icp ./icp.json --source exa --since 30d --max-searches 12 --max-usd 0.25 --save
297
- fullstackgtm signals list --since 7d # ranked triggers, each with a verbatim source quote
298
-
299
- # 2. Decide who's worth a touch — and who isn't. Scores timing × fit × memory into send/nurture/skip.
300
- fullstackgtm icp judge --signals-from latest --with-history --save
301
- fullstackgtm icp eval --golden default # gate: prove the judge is calibrated before any send (exits 2 if not)
302
-
303
- # 3. Draft the opener from the trigger. A create_task plan — proposed, never transmitted.
304
- fullstackgtm draft --from-judge latest --min-score 80 --channel email --save
305
- fullstackgtm plans approve <id> --operations all
306
- fullstackgtm apply --plan-id <id> --provider hubspot # log the touch as a CRM task
307
- fullstackgtm apply --plan-id <id> --channel outbox # OR render to the outbox for a sender — transmits nothing (docs/outbox-format.md)
308
-
309
- # 4. Close the loop. Outcomes re-weight which signals earn a touch.
310
- fullstackgtm signals outcome --account acme.com --result replied
183
+ fullstackgtm plans list
184
+ fullstackgtm plans show <id>
185
+ fullstackgtm plans approve <id> --operations <ids|all>
186
+ fullstackgtm plans reject <id>
187
+ fullstackgtm plans sync
188
+ fullstackgtm audit-log export
189
+ fullstackgtm audit-log verify
311
190
  ```
312
191
 
313
- **`signals`** is Detect-side it captures triggers into a local, profile-scoped ledger and writes *nothing* to your CRM. The five buckets (`demand`, `funding`, `job`, `company`, `social`) are not equal: a fresh round outweighs a lone social like, and a *reposted* role — the first hire fell through — outweighs a first-time post. Public ATS boards (Greenhouse, Lever, Ashby) are the free, no-auth source shipped in the box; funding/company/social arrive through `--from` staged ingest (an agent or a feed supplies them — the CLI scrapes no one), and `demand` (first-party intent) is reserved for a privileged source.
192
+ Stored plans are signed. Apply receipts preserve per-operation outcomes and provider record IDs. An interrupted provider attempt is not blindly replayed; reconcile provider state before issuing fresh authority.
314
193
 
315
- **`icp judge`** turns raw signals into a short ranked list: it scores each account on **timing × fit × memory** — the signal's weight and recency, your ICP fit, and whether you've touched the account in the last 7 days — and returns `send` / `nurture` / `skip`. Every *why-now* it produces must quote a real trigger **verbatim** (the same evidence gate as `call` and `market`); an ungrounded why-now is never stored. With no LLM key it runs a deterministic baseline. **`icp eval`** is the gate the demos skip: it grades the judge against a labeled golden set (and, once outcomes exist, checks that accounts scored hot actually book more than cold), exiting `2` below the bar so a miscalibrated judge can't reach a live send.
194
+ ## Connectors and authentication
316
195
 
317
- **`draft`** writes one opener per hot account whose first line quotes the trigger in the buyer's own words — no "Hi {{firstName}}", one ask, no manufactured urgency. It emits a `create_task` plan through the normal approval gate; it has **no send capability** and adds none — execution stays in your sender of choice. Without an LLM key it emits a clearly-labeled stub, never wooden copy passed off as a draft.
196
+ | Provider | Read | Governed writes | Notes |
197
+ |---|---:|---:|---|
198
+ | HubSpot | Yes | Yes | OAuth/broker or private-app token |
199
+ | Salesforce | Yes | Yes | Field writes and Account/Contact merges; Opportunities cannot be merged |
200
+ | Stripe | Yes | No | Restricted read key; source for revenue backfill plans |
201
+ | Apollo | Source | No direct CRM write | API key; enrichment input |
202
+ | Clay | Source | No direct CRM write | CSV/webhook ingest or Public API |
203
+ | ZoomInfo | Source | No direct CRM write | Official `gtm` CLI OAuth; enrichment input |
318
204
 
319
- **Runs on any model.** Every LLM step honors `ANTHROPIC_API_BASE_URL` / `OPENAI_API_BASE_URL`, so the same loop runs on Claude, a GLM/z.ai endpoint, or a local Ollama by pointing one env var `--model` picks the model id.
320
-
321
- ## Schedules: declare a cadence once, keep the governance contract under automation
322
-
323
- Everything the CLI produces is accurate the moment it runs and silently stale afterward. The **schedule layer** is the horizontal fix — any read/plan-side command on a cron cadence, one component, every verb (no feature owns its own cron logic):
205
+ Secrets are accepted through environment variables, stdin, the owner-only local credential store, or a paired hosted brokernever argv flags.
324
206
 
325
207
  ```bash
326
- fullstackgtm schedule add "enrich refresh --source apollo --save" --cron "0 6 * * 1" --label weekly-apollo
327
- fullstackgtm schedule add "audit --provider hubspot --save" --cron "0 2 * * *" # nightly drift baseline
328
- fullstackgtm schedule list # declarative entries; nothing runs yet
329
- fullstackgtm schedule install # materialize enabled entries into the system timer (launchd on macOS, crontab elsewhere)
330
- fullstackgtm schedule run <id> # execute now; same run record a cron firing produces
331
- fullstackgtm schedule status --runs 5 # last runs, exit codes, artifacts, next + missed firings
332
- fullstackgtm schedule uninstall # remove the managed block, touch nothing else
333
- ```
334
-
335
- **Scheduling never auto-approves.** Schedulable commands are read/plan-side only — `audit`, `snapshot`, `enrich append|refresh`, `enrich acquire --save`, `market capture|refresh`, `signals fetch|discover`, `icp judge`, `icp eval`, `draft`, `suggest`, `report`, `doctor` — so unattended runs accumulate *proposals* (plans in the queue, run records, reports), never CRM writes. `apply` is schedulable only as `apply --plan-id <id>`, and every firing re-checks the plan's status is approved: an unapproved plan records a `plan_not_approved` no-op run instead of executing, and no flag relaxes this. Arbitrary shell is not schedulable — an entry's argv must resolve to a known fullstackgtm command (validated at `add` time and re-checked at run time), and the timer line you audit — crontab entry or LaunchAgent `ProgramArguments` — is always `fullstackgtm schedule run <id>` and nothing else.
336
-
337
- `install` materializes enabled entries into the system timer; `--timer` defaults by platform. On macOS it writes one LaunchAgent plist per entry (`com.fullstackgtm.<profile>.<id>` in `~/Library/LaunchAgents`, loaded via `launchctl bootstrap`) — macOS gates `crontab` writes behind Full Disk Access, a permission no program can request, while LaunchAgents need none; launchd also coalesces firings missed during sleep into one run on wake. Re-install replaces the profile's plist fleet wholesale and never touches foreign plists. Elsewhere (or with `--timer crontab`) it renders a sentinel-delimited block (`# >>> fullstackgtm <profile> >>>` … `# <<< fullstackgtm <profile> <<<`) in your user crontab; re-install replaces the block wholesale and never touches lines outside it. Honest limitation: cron has no catch-up — a laptop asleep at firing time means a missed run. `schedule status` surfaces missed firings by comparing expected-vs-actual run history, so the gap is at least visible. Entries are provider-agnostic; cloud providers (Modal, AWS) arrive as scaffold generators that call the same `schedule run <id>` contract, and are refused as "not yet implemented" until then.
338
-
339
- ### Working across organizations
340
-
341
- Consultants and fractional operators hold credentials for several CRMs at once. A profile scopes stored logins *and* stored plans to one organization:
208
+ fullstackgtm login hubspot
209
+ fullstackgtm login salesforce --device --client-id <consumer-key>
210
+ echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
211
+ echo "$APOLLO_API_KEY" | fullstackgtm login apollo
342
212
 
343
- ```bash
344
- fullstackgtm --profile acme login hubspot
213
+ # Keep client credentials and plans isolated.
345
214
  fullstackgtm --profile acme audit --provider hubspot --save
346
- fullstackgtm profiles # list profiles, * marks the active one
215
+ FULLSTACKGTM_PROFILE=acme fullstackgtm plans list
347
216
  ```
348
217
 
349
- Set `FULLSTACKGTM_PROFILE=acme` to pin a shell (or agent sandbox) to one client. Plans saved under a profile are invisible to every other profile, so a patch plan proposed against one client's CRM can never be applied through another client's credentials.
218
+ Run `fullstackgtm doctor --json` to inspect the active profile and credential state. Detailed provider setup and data flows: [DATA-FLOWS.md](./DATA-FLOWS.md).
350
219
 
351
- ## Built for agents (and the RevOps humans they work for)
352
-
353
- Every command is designed to compose in an agent loop — deterministic output, machine-readable everywhere, meaningful exit codes:
220
+ ## Built for agents
354
221
 
355
222
  ```bash
356
- # Discover the machine-readable contract: every command, read-only vs
357
- # write-shaped access, exit codes, safety defaults (derived from the same
358
- # help table humans read, so it can't drift)
359
223
  fullstackgtm capabilities --json
360
-
361
- # Per-command help as JSON; plain --help stays human-shaped
362
224
  fullstackgtm audit --help --json
363
-
364
- # Print the shipped agent operating guide (the same SKILL.md `npx skills add` installs)
365
- fullstackgtm robot-docs
366
-
367
- # Discover what the auditor checks
368
225
  fullstackgtm rules --json
369
-
370
- # Fetch once (expensive), audit offline as many times as you like (cheap)
371
- fullstackgtm snapshot --provider hubspot --out snap.json
372
- fullstackgtm audit --input snap.json --json
373
- fullstackgtm audit --input snap.json --rules stale-deal --stale-days 45 --json
374
-
375
- # Gate a nightly CI job or agent run on hygiene: exit 2 if findings ≥ threshold
376
- fullstackgtm audit --provider hubspot --fail-on warning
377
-
378
- # Gate CI on hygiene drift instead: exit 2 only when a NEW (rule, record) finding appears
379
- fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
380
- ```
381
-
382
- - Terminal polish never leaks into machine output: color, spinners, progress bars, and sparklines render **only on an interactive TTY** (stderr for anything animated). Piped, redirected, `--json`, CI, and `NO_COLOR` output carries **zero ANSI bytes** — enforced by tests and a CI contract check — so transcripts and pipes see the same plain text they always did (`FORCE_COLOR=1` opts back in).
383
- - Finding and operation ids are **stable hashes** of rule + record, so two runs over the same data produce identical ids — agents can diff plans, track findings across runs, and approve operations by id without re-parsing.
384
- - `--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.
385
- - Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
386
-
387
- "Built for agents" is measured, not asserted: a 1,904-run benchmark (17 CRM-operations scenarios × 3 tool-surface arms × up to 4 trials, across ten models from six vendors, deterministic graders over final CRM state, τ-bench-style pass^k) shows the gated CLI surface matching or beating raw CRM-API access on completion-under-policy for every model tested — strictly better in nine of ten — and the effect is vendor-independent. Full matrix and methodology: [the leaderboard](https://github.com/fullstackgtm/core/blob/main/evals/crm/leaderboard/RESULTS.md).
388
-
389
- Caveat: the leaderboard documents the Opus reduced protocol and mixed-version Informed arms; treat the headline as a pointer to those exact RESULTS.md conditions.
390
-
391
- 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.
392
-
393
- ## Authentication: CLI-first, browser only at the consent moment
394
-
395
- Credential resolution is a ladder — the first rung that yields a token wins:
396
-
397
- 1. **`--token-env <NAME>`** — explicit env var for one invocation (agent sandboxes, scripts)
398
- 2. **`HUBSPOT_ACCESS_TOKEN` / Salesforce env** — ambient env (CI)
399
- 3. **BYO direct login** — advanced token/OAuth paths stored in `~/.fullstackgtm/credentials.json` (0600; override location with `FSGTM_HOME`); these override hosted
400
- 4. **Hosted OAuth / broker** — `fullstackgtm login hubspot` or `fullstackgtm login salesforce` opens hosted browser OAuth by default, stores only a broker credential locally, and mints provider tokens server-side
401
-
402
- ### Teams: auth once, point every CLI at the stored sync credentials
403
-
404
- ```bash
405
- fullstackgtm login --via https://gtm.yourco.com
406
- # Pairing code: ABCD-2345
407
- # Approve this CLI in your dashboard: https://gtm.yourco.com/dashboard/cli-auth?code=ABCD-2345
226
+ fullstackgtm robot-docs
408
227
  ```
409
228
 
410
- An admin connects HubSpot **once** in the hosted dashboard (the org's OAuth tokens live encrypted in the deployment). Pairing a CLI is a device-flow handshake: the CLI prints a code, an admin or manager approves it in the dashboard, and the CLI receives a long-lived broker token (stored hashed server-side, revocable per CLI). From then on, every provider command silently exchanges the broker token for a short-lived CRM access token minted from the org's stored sync credentials — and inherits the org's field mappings. No one pastes CRM tokens; revoking a laptop is one row.
229
+ Machine output is kept clean: progress stays on stderr, `--json` stays on stdout, IDs are stable, and exit codes are meaningful (`0` success, `1` error, `2` policy finding or create-gate match).
411
230
 
412
- **Run observability (paired CLIs).** Once paired, each command best-effort reports a run record command, status (`success`/`partial`/`error`), duration, headline counts (e.g. ops emitted, leads created, est. cost), and structured events (plan saved, meter charged) — to the deployment's **run timeline** in the dashboard. It's opt-in by pairing (an unpaired CLI sends nothing), a 4-second-capped POST that swallows every error, and never changes the command's exit code. Setup/inspection verbs (`login`, `doctor`, `help`, `--version`) are skipped.
231
+ The safety surface has been benchmarked across models and providers. See the [evaluation methodology and results](https://github.com/fullstackgtm/core/blob/main/evals/crm/leaderboard/RESULTS.md) for the exact protocol and caveats.
413
232
 
414
- ### Individuals: hosted OAuth by default, BYO still available
233
+ ## MCP server
415
234
 
416
235
  ```bash
417
- # Default: hosted first-party OAuth. No provider app or provider secret in the npm package.
418
- fullstackgtm login hubspot
419
- fullstackgtm login salesforce
420
- # optional: point at a self-hosted/team deployment
421
- fullstackgtm login hubspot --hosted --via https://gtm.yourco.com
422
-
423
- # Advanced HubSpot private app token (validated, then stored; token on stdin).
424
- # Note: hosted HubSpot OAuth covers reads and object writes but cannot create
425
- # tasks (HubSpot exposes no tasks scope to public apps) — use a private-app
426
- # token if your workflow relies on `create_task` proposals.
427
- echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
428
-
429
- # Advanced HubSpot BYO-app OAuth. Client secret is read from stdin/prompt — never a flag.
430
- echo "$CLIENT_SECRET" | fullstackgtm login hubspot --oauth --client-id <id>
431
- # (register http://localhost:8763/callback as a redirect URL on your app)
432
-
433
- # Advanced Salesforce BYO Connected App device flow.
434
- fullstackgtm login salesforce --device --client-id <consumer key>
435
- # ...or a session token directly (token on stdin, never as a flag):
436
- echo "$SF_SESSION_TOKEN" | fullstackgtm login salesforce --instance-url https://yourorg.my.salesforce.com
437
-
438
- fullstackgtm logout hubspot # or: salesforce | broker
439
- ```
440
-
441
- BYO direct credentials always win over a hosted broker pairing, so an operator can override the team default. First-party app secrets never ship in the npm package; hosted login stores a local broker token and mints provider tokens server-side. HubSpot does not support the device-authorization grant or secretless public clients, which is why the bring-your-own-app OAuth path requires client credentials; they are stored locally for silent refresh, the same model as `gcloud` and `aws` CLI profiles.
442
-
443
- Salesforce credential-bearing URLs are restricted to canonical HTTPS
444
- Salesforce-owned production, sandbox, and My Domain origins. Lookalike hosts,
445
- userinfo, paths/query/fragment, nonstandard ports, unsafe token-response
446
- instance URLs, and credential-forwarding redirects are refused both when the
447
- credential is stored and every time it is used.
448
-
449
- ## Connect your CRM
450
-
451
- What each provider actually requires before `audit --provider <name>` works on your data.
452
-
453
- ### Connector capabilities
454
-
455
- Connectors differ in what the provider's API allows — stated up front so nothing surprises you mid-evaluation:
456
-
457
- | Operation | HubSpot | Salesforce | Stripe |
458
- | --- | --- | --- | --- |
459
- | Read / snapshot / audit | ✅ | ✅ | ✅ (read-only) |
460
- | Field writes (`set_field`, `clear_field`, `link_record`) | ✅ | ✅ | — |
461
- | `create_task` | ✅ | ✅ | — |
462
- | `archive_record` | ✅ | ✅ | — |
463
- | `merge_records` (`dedupe`) | ✅ | ✅ Account/Contact (SOAP); ❌ Opportunity | — |
464
-
465
- **Salesforce merge** uses the SOAP `merge()` call (REST has no merge resource), so `dedupe` works on Salesforce **Accounts and Contacts** — the OAuth token doubles as the SOAP session, and groups larger than three records are merged in batches (master + 2 per call). **Opportunities cannot be merged** (Salesforce exposes no opportunity merge in the API or the UI) — for duplicate opportunities, pick a survivor and close/archive the rest. As always, merges are irreversible and go through the same approval gate + drift guard as every other write. (Validate against a sandbox first if you're wiring it into automation — the SOAP path is exercised by unit tests but a live Salesforce org is the real proof.)
466
-
467
- ### HubSpot: create a private app (~2 minutes, needs super-admin)
468
-
469
- 1. In HubSpot: **Settings → Integrations → Private Apps → Create a private app.**
470
- 2. On the **Scopes** tab, grant the read scopes the audit needs:
471
- - `crm.objects.owners.read`
472
- - `crm.objects.companies.read`
473
- - `crm.objects.contacts.read`
474
- - `crm.objects.deals.read`
475
- 3. If you plan to **apply** approved operations (not just audit), also grant write scopes for the objects you'll let it touch: `crm.objects.deals.write` (covers `deal.next_step` and other deal fields), plus `crm.objects.contacts.write` / `crm.objects.companies.write` for contact/company patches, and the **Tasks** write scope for `create_task` operations (search "tasks" in the scope picker; naming varies by portal).
476
- 4. Create the app, copy the token (`pat-...`), then: `echo "$TOKEN" | fullstackgtm login hubspot --private-token`.
477
-
478
- If a scope is missing you'll see a `403` mid-run whose body names the exact missing scope (`requiredGranularScopes`) — add it to the private app and re-run. Note that `login` only validates the token itself; it can't tell whether every scope you'll need is granted.
479
-
480
- ### Salesforce: a Connected App (one-time, usually needs an admin)
481
-
482
- Device-flow login requires a Connected App in your org — if you're not an admin, this is the step to ask one for:
483
-
484
- 1. **Setup → App Manager → New Connected App**, enable OAuth settings.
485
- 2. Check **Enable Device Flow**.
486
- 3. OAuth scopes: **Manage user data via APIs (`api`)** and **Perform requests at any time (`refresh_token`)** — the CLI requests exactly these.
487
- 4. Save (Salesforce can take ~2–10 minutes to propagate a new Connected App), copy the **Consumer Key**, then: `fullstackgtm login salesforce --device --client-id <consumer key>`.
488
-
489
- Writeback needs no extra OAuth scope — applies are gated by the logged-in user's normal object/field permissions.
490
-
491
- **Quickest token for a one-off or sandbox test (the `sf` CLI).** If you already have the [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) authenticated to an org, you can skip the Connected App entirely and feed a session token straight in:
236
+ # Zero-install wrapper with MCP peers bundled
237
+ npx -y fullstackgtm-mcp
492
238
 
493
- ```bash
494
- # --verbose is required to include the access token. Note: the command prints a
495
- # sensitive-info warning *before* the JSON, so slice from the first '{' when parsing.
496
- sf org display --target-org <alias> --verbose --json
497
- # then hand the accessToken + instanceUrl to the CLI (token on stdin, never a flag):
498
- echo "$ACCESS_TOKEN" | fullstackgtm login salesforce --instance-url "$INSTANCE_URL"
239
+ # Or in a project
240
+ npm install fullstackgtm @modelcontextprotocol/sdk zod
241
+ npx fullstackgtm-mcp
499
242
  ```
500
243
 
501
- This is a **short-lived session token with no refresh** — ideal for a one-off audit or a sandbox parity test, but it expires within hours and `doctor` will show it as a static login. For durable, unattended use, set up the Connected App device flow above instead (it refreshes silently).
244
+ Nine tools are exposed over stdio.
502
245
 
503
- ### Stripe: a restricted key is enough (read-only connector)
246
+ **Read-only:** `fullstackgtm_audit`, `fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`, `fullstackgtm_call_parse`, `fullstackgtm_resolve`, and `fullstackgtm_market_worksheet`.
504
247
 
505
- The Stripe connector reads customers, subscriptions, and paid invoices (`backfill stripe` calls `/v1/invoices`), and `apply` is read-only by construction. Create a **restricted key** with just **Customers: Read**, **Subscriptions: Read**, and **Invoices: Read** (Developers API keys Create restricted key) instead of pasting a full-access secret key: `echo "$KEY" | fullstackgtm login stripe`.
248
+ **Gated:** `fullstackgtm_apply` accepts only a stored plan ID whose operation approvals and values are already HMAC-signed in the plan store; `fullstackgtm_market_observe` verifies quoted evidence before storage.
506
249
 
507
- ## Concepts
250
+ The MCP caller cannot approve its own operations, inject an external plan, or execute rule-package code. See [docs/api.md](./docs/api.md) for the public contracts.
508
251
 
509
- | Concept | What it is |
510
- |---|---|
511
- | **Canonical snapshot** | Provider-independent view of users, accounts, contacts, deals, activities. Records carry `identities` — `(provider, externalId)` claims — so the same real-world entity can be tracked across several systems. |
512
- | **Audit rule** | A deterministic function `(context) => { findings, operations }`. Twelve built-ins cover orphan accounts, ownerless/unlinked/amount-less deals, past close dates, stale pipeline, duplicates, and more — `fullstackgtm rules` lists them all. Write your own in ~10 lines. |
513
- | **Patch plan** | The dry-run output of an audit: findings plus typed patch operations with before/after values, reasons, risk levels, and approval flags. Always a proposal, never a mutation. |
514
- | **Connector** | A provider adapter: `fetchSnapshot()` for reads, optional `applyOperation()` for writes. HubSpot and Salesforce reference connectors ship in the package; connectors never drop records they can't fully resolve — the audit flags them instead. |
515
- | **Patch plan run** | The audit record of one apply attempt: per-operation applied/failed/skipped results. |
252
+ ## Library
516
253
 
517
- ## Write a custom rule
518
-
519
- ```ts
520
- import {
521
- auditSnapshot, auditFindingId, builtinAuditRules, defaultPolicy,
522
- type GtmAuditRule,
523
- } from "fullstackgtm";
524
-
525
- const missingAmount: GtmAuditRule = {
526
- id: "missing-deal-amount",
527
- title: "Deal has no amount",
528
- description: "Amountless deals make forecast coverage meaningless.",
529
- evaluate: ({ snapshot }) => ({
530
- findings: snapshot.deals
531
- .filter((deal) => !deal.amount)
532
- .map((deal) => ({
533
- id: auditFindingId("missing-deal-amount", deal.id),
534
- objectType: "deal", objectId: deal.id,
535
- ruleId: "missing-deal-amount",
536
- title: "Deal has no amount", severity: "warning",
537
- summary: `${deal.name} has no amount.`,
538
- recommendation: "Set an amount or close the deal out.",
539
- })),
540
- operations: [],
541
- }),
542
- };
543
-
544
- const plan = auditSnapshot(snapshot, defaultPolicy(), [...builtinAuditRules, missingAmount]);
545
- ```
546
-
547
- ## Use a connector programmatically
254
+ The framework-independent package exports the same canonical model, rule engine, plan contract, connectors, and apply orchestrator used by the CLI.
548
255
 
549
256
  ```ts
550
257
  import { applyPatchPlan, auditSnapshot, createHubspotConnector } from "fullstackgtm";
@@ -554,89 +261,36 @@ const hubspot = createHubspotConnector({
554
261
  });
555
262
 
556
263
  const snapshot = await hubspot.fetchSnapshot();
557
- const plan = auditSnapshot(snapshot);
264
+ const plan = auditSnapshot(snapshot); // read-only
558
265
 
559
- // Later, after human review:
266
+ // Later, after review:
560
267
  const run = await applyPatchPlan(hubspot, plan, {
561
268
  approvedOperationIds: ["op_abc123"],
562
- valueOverrides: { op_abc123: "9001" },
563
269
  });
564
270
  ```
565
271
 
566
- Implementing a new provider means implementing one type:
567
-
568
- ```ts
569
- import type { GtmConnector } from "fullstackgtm";
570
-
571
- const myConnector: GtmConnector = {
572
- provider: "my-crm",
573
- fetchSnapshot: async () => ({ /* canonical snapshot */ }),
574
- applyOperation: async (operation) => ({ operationId: operation.id, status: "applied" }),
575
- };
576
- ```
577
-
578
- ## MCP server
579
-
580
- The MCP entrypoint needs the optional peer dependencies `@modelcontextprotocol/sdk` and `zod` — plain `npx fullstackgtm-mcp` won't install optional peers, so pull them in explicitly:
581
-
582
- ```bash
583
- # In a project
584
- npm install fullstackgtm @modelcontextprotocol/sdk zod
585
- HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm-mcp
586
-
587
- # Zero-install (the fullstackgtm-mcp wrapper package bundles the MCP peers)
588
- npx -y fullstackgtm-mcp
589
- ```
590
-
591
- Add it to Claude Code in one command:
272
+ Implement another CRM by satisfying the `GtmConnector` contract. Add deterministic organization policy with a `GtmAuditRule`. Architecture and extension points are documented in [docs/architecture.md](./docs/architecture.md) and [docs/api.md](./docs/api.md).
592
273
 
593
- ```bash
594
- claude mcp add fullstackgtm -e HUBSPOT_ACCESS_TOKEN=pat-... -- npx -y fullstackgtm-mcp
595
- ```
596
-
597
- Or configure any MCP client (Cursor, Claude Desktop, …) with:
598
-
599
- ```json
600
- {
601
- "mcpServers": {
602
- "fullstackgtm": {
603
- "command": "npx",
604
- "args": ["-y", "fullstackgtm-mcp"],
605
- "env": { "HUBSPOT_ACCESS_TOKEN": "pat-..." }
606
- }
607
- }
608
- }
609
- ```
274
+ ## Appendix: additional modules and recipes
610
275
 
611
- Nine tools are exposed over stdio.
612
-
613
- **Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_capabilities` (server/tool capability manifest), `fullstackgtm_rules` (rule discovery), `fullstackgtm_suggest` (deterministic placeholder values with confidence + reasons), `fullstackgtm_call_parse` (transcripts → provenance-marked segments, insights, and evidence), `fullstackgtm_resolve` (the create gate: exists / ambiguous / safe_to_create), and `fullstackgtm_market_worksheet` (the classification packet for one vendor: claims, judging rules, captured page texts).
614
-
615
- **Gated:** `fullstackgtm_apply` (accepts only a stored plan id; approvals and values must already be human-approved and HMAC-signed in the plan store) and `fullstackgtm_market_observe` (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).
616
-
617
- Tokens stored via `fullstackgtm login` are picked up automatically — the env var is only needed when no stored login exists.
276
+ The package includes composable GTM workflows, but they are intentionally downstream of the control-plane story above. None bypasses the patch-plan safety contract.
618
277
 
619
- Rule packages are executable JavaScript and are disabled by default. In the CLI,
620
- review the modules and use `--config <path> --allow-plugins`; use `--no-plugins`
621
- to apply only declarative config. MCP never executes rule packages: mutable
622
- tool-call paths are not a durable code-trust boundary.
278
+ - **Call evidence:** parse, classify, score, and link transcripts; convert next steps into governed CRM plans. Run `fullstackgtm call --help`.
279
+ - **ICP and net-new acquire:** discover, score, dedupe, meter, assign, and propose new contacts. See [docs/recipes.md](./docs/recipes.md).
280
+ - **Signals and drafting:** collect buying triggers, judge timing/fit, and produce governed task/outbox plans; the package never sends. See [docs/signal-spool-format.md](./docs/signal-spool-format.md) and [docs/outbox-format.md](./docs/outbox-format.md).
281
+ - **TAM:** estimate a transparent market model, populate it through governed acquire, and track coverage. See [docs/tam.md](./docs/tam.md).
282
+ - **Competitive market map:** capture and verify category evidence, classify messaging fronts, and generate reports. Run `fullstackgtm market --help`.
283
+ - **LinkedIn list intake:** read prebuilt HeyReach lists as a discovery source; no connect/message operation ships. See [docs/linkedin-connector-spec.md](./docs/linkedin-connector-spec.md).
284
+ - **Scheduling:** run read/plan-side commands continuously without auto-approval. Run `fullstackgtm schedule --help`.
285
+ - **End-to-end playbooks:** [docs/recipes.md](./docs/recipes.md).
623
286
 
624
- ## Safety model
287
+ ## License, boundary, and development
625
288
 
626
- 1. Reads are safe by default; audits never mutate anything.
627
- 2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
628
- 3. `applyPatchPlan` enforces the contract for all connectors: only explicitly approved operation ids are written, placeholders require concrete override values, and every attempt produces a per-operation result record.
289
+ Licensed under [Apache-2.0](./LICENSE). The canonical model, audits, patch plans, connectors, CLI, and MCP server are open source. The hosted Full Stack GTM dashboard, sync backend, broker, and team workflows are a separate proprietary product built on the package. Features never move from open to closed.
629
290
 
630
- ## License & boundary
631
-
632
- Licensed under [Apache-2.0](./LICENSE). The boundary is deliberate and stable: the framework, CLI, and MCP server are open source; the hosted Full Stack GTM application (dashboard, sync backend, broker service, team workflows) is a separate, proprietary product built on top of this package. Features never move from open to closed. See [CONTRIBUTING.md](./CONTRIBUTING.md) for how development and mirroring work.
633
-
634
- The surfaces in [docs/api.md](./docs/api.md) — the canonical model, rule interface, plan/apply contract, connector contract, merge/diff, config, CLI, and MCP tools — are settling but may still break in minor releases until 1.0. The safety invariants (read-only audits, approval-gated writes, placeholder refusal) are not beta and do not change.
635
-
636
- ## Development
291
+ Public API stability and the open-core boundary are documented in [docs/api.md](./docs/api.md) and [CONTRIBUTING.md](./CONTRIBUTING.md).
637
292
 
638
293
  ```bash
639
- npm run build # compiles src/ to dist/ (tsc, type declarations included)
294
+ npm run build
295
+ npm test
640
296
  ```
641
-
642
- Tests live in the repository root `tests/` directory and run with `npm test`.