fullstackgtm 0.42.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 (68) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/README.md +18 -7
  3. package/dist/calls.d.ts +13 -0
  4. package/dist/calls.js +14 -3
  5. package/dist/cli.js +718 -60
  6. package/dist/connectors/hubspot.js +58 -24
  7. package/dist/connectors/outboxChannel.d.ts +64 -0
  8. package/dist/connectors/outboxChannel.js +170 -0
  9. package/dist/connectors/prospectSources.d.ts +22 -0
  10. package/dist/connectors/prospectSources.js +43 -0
  11. package/dist/connectors/salesforce.js +40 -15
  12. package/dist/connectors/signalSources.d.ts +105 -0
  13. package/dist/connectors/signalSources.js +316 -0
  14. package/dist/connectors/theirstack.d.ts +84 -0
  15. package/dist/connectors/theirstack.js +125 -0
  16. package/dist/draft.js +27 -5
  17. package/dist/enrich.d.ts +6 -0
  18. package/dist/enrich.js +47 -1
  19. package/dist/health.d.ts +12 -0
  20. package/dist/health.js +29 -2
  21. package/dist/icp.d.ts +47 -3
  22. package/dist/icp.js +105 -11
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/init.d.ts +47 -0
  26. package/dist/init.js +143 -0
  27. package/dist/judge.d.ts +36 -3
  28. package/dist/judge.js +46 -10
  29. package/dist/judgeEval.d.ts +7 -0
  30. package/dist/judgeEval.js +8 -1
  31. package/dist/runReport.d.ts +26 -0
  32. package/dist/runReport.js +15 -0
  33. package/dist/schedule.js +18 -4
  34. package/dist/signals.d.ts +58 -0
  35. package/dist/signals.js +65 -0
  36. package/dist/tam.d.ts +225 -0
  37. package/dist/tam.js +470 -0
  38. package/dist/types.d.ts +12 -0
  39. package/docs/api.md +26 -4
  40. package/docs/outbox-format.md +92 -0
  41. package/docs/recipes.md +195 -0
  42. package/docs/roadmap-to-1.0.md +31 -1
  43. package/docs/signal-spool-format.md +162 -0
  44. package/docs/tam.md +195 -0
  45. package/llms.txt +89 -1
  46. package/package.json +1 -1
  47. package/skills/fullstackgtm/SKILL.md +17 -1
  48. package/src/calls.ts +24 -3
  49. package/src/cli.ts +809 -55
  50. package/src/connectors/hubspot.ts +55 -23
  51. package/src/connectors/outboxChannel.ts +202 -0
  52. package/src/connectors/prospectSources.ts +57 -0
  53. package/src/connectors/salesforce.ts +39 -14
  54. package/src/connectors/signalSources.ts +363 -0
  55. package/src/connectors/theirstack.ts +170 -0
  56. package/src/draft.ts +26 -5
  57. package/src/enrich.ts +51 -1
  58. package/src/health.ts +47 -2
  59. package/src/icp.ts +113 -11
  60. package/src/index.ts +42 -0
  61. package/src/init.ts +166 -0
  62. package/src/judge.ts +85 -11
  63. package/src/judgeEval.ts +8 -1
  64. package/src/runReport.ts +39 -0
  65. package/src/schedule.ts +20 -4
  66. package/src/signals.ts +95 -0
  67. package/src/tam.ts +654 -0
  68. package/src/types.ts +12 -0
package/llms.txt CHANGED
@@ -15,10 +15,12 @@ at/above `--fail-on`.
15
15
  - [README](https://github.com/fullstackgtm/core/blob/main/README.md): install, five-minute loop, auth ladder, MCP setup, programmatic use
16
16
  - [INSTALL_FOR_AGENTS](https://github.com/fullstackgtm/core/blob/main/INSTALL_FOR_AGENTS.md): deterministic install-and-verify steps with expected outputs
17
17
  - [Agent skill](https://github.com/fullstackgtm/core/blob/main/skills/fullstackgtm/SKILL.md): compact operating guide, installable via `npx skills add fullstackgtm/core`
18
+ - [Recipes](https://github.com/fullstackgtm/core/blob/main/packages/fullstackgtm/docs/recipes.md): five composable GTM plays over the primitives (cold-start lead-fill, the trigger→judge→draft outbound loop, scheduled-continuous, ABM-from-companies, hygiene-gated outbound) — the CLI ships primitives, your agent is the orchestrator, the package never sends
18
19
  - [Architecture](https://github.com/fullstackgtm/core/blob/main/docs/architecture.md): module map + snapshot → audit → plan → apply data flow; "where do I add a rule/connector/operation"
19
20
  - [Contributing](https://github.com/fullstackgtm/core/blob/main/CONTRIBUTING.md): dev setup, the open-core mirror model, the release ritual
20
21
  - [API reference](https://github.com/fullstackgtm/core/blob/main/docs/api.md): semver-covered surfaces — canonical model, rule interface, plan/apply contract, connector contract, config, CLI, MCP tools
21
22
  - [CRM-health lifecycle](https://github.com/fullstackgtm/core/blob/main/docs/crm-health-lifecycle.md): the Prevent → Detect → Remediate → Verify/Attribute model; no-new-dupes design
23
+ - [TAM mapping](https://github.com/fullstackgtm/core/blob/main/docs/tam.md): size the reachable market from the ICP, then populate it with scheduled governed acquire and track coverage to an ETA
22
24
  - [CHANGELOG](https://github.com/fullstackgtm/core/blob/main/CHANGELOG.md): release history
23
25
 
24
26
  `fullstackgtm resolve <type>` is the create gate: exit 0 = safe to create,
@@ -57,6 +59,60 @@ uncertainty; report bubbles ∝ estimated share only when signals exist.
57
59
  Storage is profile-scoped under `<home>/market/<category>`. MCP:
58
60
  `fullstackgtm_market_worksheet`, `fullstackgtm_market_observe`.
59
61
 
62
+ ## Key invariants (TAM mapping)
63
+
64
+ `fullstackgtm tam` sizes the reachable market FROM the ICP and then iteratively
65
+ fills it — not a one-shot headline. `tam estimate` computes a transparent model:
66
+ account universe × ACV = TAM (account/per-logo basis by default; `--acv-basis
67
+ buyer` for per-seat = contacts × ACV), where contacts = accounts ×
68
+ buyers/account is the population target. The account count is `--accounts <n>`
69
+ (assumption), OR `--source theirstack` — the TECHNOGRAPHIC universe: counts
70
+ companies that USE a CRM/MAP (`firmographics.technologies` → TheirStack
71
+ `company_technology_slug_or`) + geo + employee bounds; the real RevOps buying signal
72
+ AND the only source that returns a LIST (`tam accounts --source theirstack` pulls
73
+ real companies name/domain, `--out csv`/`--json`, `--max` caps credit spend; a
74
+ count is cheap but NOT free — limit:0 is rejected so a count returns 1 row ≈ 3
75
+ credits, a list pull ≈ 3/company; `login theirstack`). OR `--source explorium` — a firmographic
76
+ count via `/v1/businesses` (`total_results`), a count ONLY (Explorium 403s on
77
+ list-pull, can't target by CRM-usage; must OMIT `size` or it caps the total; capped
78
+ at 60,000 `EXPLORIUM_BUSINESS_COUNT_CAP` → `provider:explorium (≥60k cap — floor)` +
79
+ ⚠). Verified live: people endpoints can't size a market (Explorium `/v1/prospects`
80
+ total == page size; pipe0/Crustdata returns only a cursor — `--source pipe0` for
81
+ estimate is REJECTED, it's a population source). The model always LABELS
82
+ `accountsSource` (provider:theirstack/explorium vs assumption). ACV must be a
83
+ real ANNUAL figure the operator CONFIRMS — `--acv <annual-usd>`, OR `--acv-from-crm
84
+ --deal-period monthly|quarterly|annual` which takes the median closed-won amount and
85
+ ANNUALIZES it (the period is required — a deal can be MRR/quarterly/annual and
86
+ guessing is a 4–12× error; $15k/mo → $180k). NO band defaults, and a bare
87
+ `--provider` does NOT auto-set ACV (HubSpot is the COVERAGE source, not the ACV) —
88
+ `estimate` REFUSES without a confirmed ACV. Buyers/account is `--buyers-per-account`
89
+ OR the CRM's avg-contacts-per-account OR a labeled `assumption:1-buyer`. Both
90
+ `acv.source` and `universe.buyersSource` are labeled (incl. deal count + the
91
+ period×factor, so a thin signal like "1 deal" is visible). Optional
92
+ citable cross-checks (`{claim,valueUsd?,sourceUrl,quote}`) sit beside the
93
+ bottom-up number as a top-down sanity check. `estimate`
94
+ records a coverage BASELINE (CRM counts now) so `status` attributes what the
95
+ campaign added. `tam populate --cron` schedules `enrich acquire --source <s>
96
+ --save` — the ONE create-side verb the schedule allowlist permits, and only in
97
+ its plan-producing `--save` form: each firing queues a needs_approval lead plan,
98
+ the acquire meter is charged only at apply, and apply stays `apply --plan-id`
99
+ (re-checked approved). `tam status` measures coverage on the INTERSECTION of CRM ×
100
+ TAM-ICP, not a raw count: `tam estimate` stores the ICP filter (`model.targeting`),
101
+ and status CLASSIFIES each domain-bearing account into in-TAM / out-of-TAM / unknown
102
+ (checked on size+industry — geo + "uses-CRM" aren't on a CanonicalAccount, so a
103
+ `--reverify` re-enrich is future; the classifier labels what it checked). ONLY
104
+ in-TAM counts toward coverage, so off-ICP accounts loaded from elsewhere can't
105
+ inflate it. Bottom-up vs top-down reconciliation: the in-TAM count is a FLOOR on the
106
+ real universe (those are verified members), so when in-TAM ≥ the estimate, status
107
+ stops reporting a fake 100% and flips to "estimate was a floor — real market is at
108
+ least N (`reconciledUniverse = max(estimate, inTam)`); re-estimate for the headroom".
109
+ `--save` stamps an append-only timeline. `tam report` (markdown) and `status` project
110
+ an ETA on the IN-TAM accounts/day burn across the timeline → days-remaining → fill
111
+ date — and REFUSE to project (honest "not enough history") below two readings, a flat
112
+ rate, or zero elapsed time.
113
+ Coverage + ETA are deterministic (no LLM, byte-stable). Storage profile-scoped
114
+ under `<home>/tam/<name>/` (`model.json` + `coverage.jsonl`, 0600).
115
+
60
116
  ## Key invariants (governed write verbs)
61
117
 
62
118
  `bulk-update <object> --where … (--set|--archive|--create-task)` filters the
@@ -117,12 +173,44 @@ injectable `LinkedInProvider` (default HeyReach; `FakeLinkedInProvider` for test
117
173
  LinkedIn is read-only — Phase 1 has no `linkedin_connect`/`linkedin_message` ops
118
174
  and no webhook ingestion (Phase 2). Never auto-writes.
119
175
 
176
+ ## Key invariants (GTM brain — signals / icp / judge / draft)
177
+
178
+ The timing/outbound layer turns "who changed" into one grounded, governed
179
+ opener — and **never sends**. `signals fetch` captures fresh buying triggers
180
+ into a profile-scoped ledger (free Greenhouse/Lever/Ashby ATS scrapers in the
181
+ box; funding/social via `--from`; connected platforms via `--connector`
182
+ file/serpapi-news/hubspot-forms with secrets through the credential ladder; and
183
+ push platforms via the webhook spool — `--connector file` reads the conventional
184
+ landing zone `<home>/signals/spool/*.jsonl`, see docs/signal-spool-format.md),
185
+ keyed by **account domain**, ranked by learned `weights`; it writes NOTHING to
186
+ the CRM. `icp interview|set|show`
187
+ builds `icp.json`; `icp judge` ranks fresh signals into send/nurture/skip — pass
188
+ a snapshot (`--provider`/`--input`/`--demo`) and each decision resolves its CRM
189
+ target: `accountId`, the best `contact {id,email,title}`, and `contacts[]` (all
190
+ in-CRM contacts at the account, titled-first, cap 10, for multi-threading);
191
+ `--with-history` additionally gates memory (don't re-touch a recently-touched
192
+ account) and fit-scoring. `icp eval` is the calibration gate (exit 2 below the
193
+ bar). `draft` emits ONE trigger-grounded opener per hot account as a governed
194
+ `create_task` targeting the resolved `contact.id` (or `accountId`) — a
195
+ domain-only decision (account not yet in the CRM) is REJECTED with "acquire it
196
+ first" (run `enrich acquire`, re-judge with the snapshot). `--channel
197
+ email|linkedin|task` shapes the opener. After `plans approve`, applying the draft
198
+ plan logs the touch as a CRM task (`apply --provider <crm>`) OR renders it to the
199
+ governed outbox (`apply --channel outbox` → `<home>/signals/outbox/<channel>.jsonl`
200
+ for a downstream sender to drain). Either way the CLI **transmits nothing** — the
201
+ outbox is the send-side mirror of the spool; the always-on sender is hosted/opt-in. `signals outcome --account <domain>
202
+ --contact <id> --result replied|...` credits the contact reached so weights
203
+ re-learn. The contact↔account join (`contact.accountId` + `account.domain`) is
204
+ surfaced at every hop, so the loop is contact-coherent end to end — see the
205
+ [recipes](https://github.com/fullstackgtm/core/blob/main/packages/fullstackgtm/docs/recipes.md).
206
+
120
207
  ## Key invariants (schedule)
121
208
 
122
209
  `fullstackgtm schedule` is the horizontal scheduler — no feature namespace
123
210
  owns cron logic. `add "<command>" --cron "<expr>"` validates the command
124
211
  against the read/plan-side allowlist (audit, snapshot, enrich append|refresh,
125
- market capture|refresh, suggest, report, doctor) and the 5-field cron
212
+ enrich acquire --save, market capture|refresh, signals fetch, icp judge|eval,
213
+ draft, suggest, report, doctor) and the 5-field cron
126
214
  expression, but touches no crontab; `install` materializes enabled entries
127
215
  into a sentinel-delimited managed block (`# >>> fullstackgtm <profile> >>>`)
128
216
  replaced wholesale on re-install, lines outside it untouched; `uninstall`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
@@ -62,8 +62,12 @@ credentials AND stored plans per client org.
62
62
  | `fix --rule <id>` | audit one rule → suggest → approve at the confidence bar → apply only with `--yes` |
63
63
  | `call parse\|score\|link\|plan` | Transcripts → evidence-quoted insights, rubric scorecards, deal linking, governed next-step writes |
64
64
  | `enrich append\|refresh\|ingest\|status` | Governed enrichment (Apollo pull / Clay ingest), fill-blanks-only plans |
65
- | `enrich acquire [--source explorium\|pipe0\|linkedin]` | Net-new ICP-targeted lead gen: resolve-first deduped `create_record` plans, capped by a per-profile windowed meter (records + spend); owner-stamped via `acquire.assign`/`--assign-owner` (never born ownerless); `--source linkedin --list <id>` reads a HeyReach lead list (Phase 1 discovery, read-only — never sends); never auto-writes |
65
+ | `enrich acquire [--source explorium\|pipe0\|linkedin]` | Net-new ICP-targeted lead gen: resolve-first deduped `create_record` plans, capped by a per-profile windowed meter (records + spend); owner-stamped via `acquire.assign`/`--assign-owner` (never born ownerless); links each lead to a domain-stamped, signal-watchable account; `acquire.create.company` acquires accounts (ABM); `--source linkedin --list <id>` reads a HeyReach lead list (Phase 1 discovery, read-only — never sends); never auto-writes |
66
+ | `signals fetch\|list\|outcome\|weights` | Detect-side timing layer: capture fresh buying triggers into a profile-scoped ledger (free Greenhouse/Lever/Ashby ATS in the box; `--from` staged files; `--connector file\|serpapi-news\|hubspot-forms` to pull connected platforms / read the webhook spool `<home>/signals/spool`, secrets via the credential ladder), ranked by learned weights. Writes NOTHING to the CRM; `outcome --account <domain> --contact <id> --result …` re-weights which triggers earn a touch |
67
+ | `icp interview\|set\|show\|judge\|eval` | Build the ICP, then `judge` ranks fresh signals into send/nurture/skip — pass a snapshot (`--provider`/`--input`) and each decision resolves its CRM target (`accountId` + the `contact`/`contacts` to reach). `eval` is the calibration gate (exit 2 below the bar) |
68
+ | `draft [--from-judge latest] [--channel email\|linkedin\|task]` | One trigger-grounded opener per hot account → a governed `create_task` targeting the resolved `contact.id` (or `accountId`); rejects a domain-only decision ("acquire it first"). **Never sends** — after `plans approve`, `apply --provider <crm>` logs it as a CRM task, or `apply --channel outbox` renders it to `<home>/signals/outbox/<channel>.jsonl` for a downstream sender to drain; the CLI transmits nothing |
66
69
  | `market init\|capture\|classify\|worksheet\|observe\|fronts\|axes\|overlay\|scale\|report\|refresh` | Competitive category map; evidence quotes verified verbatim against stored captures |
70
+ | `tam estimate\|accounts\|status\|report\|populate` | Size the reachable market FROM the ICP (real account count × real ACV; buyers/account → contact target), then iteratively fill it: `populate` schedules plan-only `enrich acquire --save` (apply stays gated), `status` classifies CRM accounts vs the TAM ICP into **in-TAM / out-of-TAM / unknown** (only in-TAM counts — junk loaded from elsewhere doesn't inflate coverage; checked on size+industry, geo/tech need re-enrich) and flips to "estimate was a FLOOR, revise up" when bottom-up in-CRM exceeds the top-down estimate; `--save` stamps a timeline, `report` projects burn-up + ETA on the in-TAM rate. **`estimate --source theirstack`** counts companies that USE a CRM/MAP (`firmographics.technologies`) — the real RevOps universe — and **`tam accounts --source theirstack`** pulls the actual company LIST (names+domains, `--out csv`) at ~3 credits/company — `--dry-run [--usd-per-credit <r>]` prices the pull + full TAM for 0 credits, `--confirm` gates pulls above `--max-credits` (default 150). `--source explorium` = firmographic count only (no list, 60k cap = floor); else `--accounts`. **ACV must be a real ANNUAL figure you confirm** — `--acv <annual-usd>`, or `--acv-from-crm --deal-period monthly\|quarterly\|annual` (median closed-won, annualized); NO band defaults, a bare `--provider` does NOT set ACV (HubSpot=coverage), refuses without one. Buyers/account = CRM avg-contacts or explicit. Every number labeled with its source |
67
71
  | `schedule add\|list\|remove\|enable\|disable\|run\|install\|uninstall\|status` | Horizontal cron; read/plan-side allowlist only — scheduling NEVER auto-approves |
68
72
  | `report` | Client-ready audit deliverable (markdown or self-contained HTML) |
69
73
  | `health [--json]` | Per-profile CRM health timeline: deterministic 0–100 score, trend, per-rule deltas — accrues from `audit --save`, read-only |
@@ -84,8 +88,20 @@ Tools over stdio: `fullstackgtm_audit` (read-only), `fullstackgtm_rules`,
84
88
  `fullstackgtm_resolve`, `fullstackgtm_market_worksheet`,
85
89
  `fullstackgtm_market_observe`.
86
90
 
91
+ ## Composing the primitives into plays
92
+
93
+ This CLI ships governed **primitives** — there is no `outbound` mega-command.
94
+ **You (the agent) are the orchestrator:** chain these verbs into the play the
95
+ operator wants, surface the one approve gate, and bridge the last mile to the
96
+ sender (**the package never sends**). [docs/recipes.md](https://github.com/fullstackgtm/core/blob/main/packages/fullstackgtm/docs/recipes.md)
97
+ has five worked plays — cold-start lead-fill, the trigger→judge→draft outbound
98
+ loop, scheduled-continuous, ABM-from-companies, and hygiene-gated outbound.
99
+ `fullstackgtm init` scaffolds a workspace (icp.json + enrich config + a PLAYBOOK
100
+ pointing at those recipes) to start from.
101
+
87
102
  ## Going deeper
88
103
 
104
+ - [docs/recipes.md](https://github.com/fullstackgtm/core/blob/main/packages/fullstackgtm/docs/recipes.md) — five composable GTM plays over the primitives (cold-start, outbound loop, scheduled, ABM, hygiene-gated)
89
105
  - [llms.txt](https://github.com/fullstackgtm/core/blob/main/llms.txt) — the full invariant map per layer (calls, market, write verbs, enrich, schedule, engagement/health)
90
106
  - [INSTALL_FOR_AGENTS.md](https://github.com/fullstackgtm/core/blob/main/INSTALL_FOR_AGENTS.md) — deterministic install-and-verify with expected outputs
91
107
  - [docs/api.md](https://github.com/fullstackgtm/core/blob/main/docs/api.md) — semver-covered surfaces: canonical model, rule interface, plan/apply contract, connectors, config, CLI, MCP
package/src/calls.ts CHANGED
@@ -369,6 +369,15 @@ export type CallDealSuggestion = {
369
369
  dealName?: string;
370
370
  accountId?: string;
371
371
  accountName?: string;
372
+ /**
373
+ * HOW the deal's account was matched — `account_domain` (the company-wide,
374
+ * reliable signal) vs `contact_email` (a specific person, possibly stale) vs
375
+ * `both`. An agent needs this to weight the match; the bare confidence hides it.
376
+ */
377
+ resolvedVia?: "account_domain" | "contact_email" | "both";
378
+ /** The attendee contacts that matched (the email path), so the contact↔account
379
+ * hop is visible, not just the resolved account. */
380
+ matchedContacts?: { id: string; email?: string; accountId?: string }[];
372
381
  confidence: "high" | "low" | "none";
373
382
  reason: string;
374
383
  };
@@ -393,17 +402,27 @@ export function suggestCallDeal(
393
402
  }
394
403
 
395
404
  const accountIds = new Set<string>();
405
+ let viaDomain = false;
406
+ let viaEmail = false;
407
+ const matchedContacts: { id: string; email?: string; accountId?: string }[] = [];
396
408
  for (const account of snapshot.accounts) {
397
409
  const domain = account.domain?.trim().toLowerCase().replace(/^www\./, "");
398
- if (domain && domains.has(domain)) accountIds.add(account.id);
410
+ if (domain && domains.has(domain)) {
411
+ accountIds.add(account.id);
412
+ viaDomain = true;
413
+ }
399
414
  }
400
415
  for (const contact of snapshot.contacts) {
401
416
  const email = contact.email?.trim().toLowerCase();
402
417
  const at = email ? email.indexOf("@") : -1;
403
418
  if (email && at > 0 && domains.has(email.slice(at + 1)) && contact.accountId) {
404
419
  accountIds.add(contact.accountId);
420
+ viaEmail = true;
421
+ matchedContacts.push({ id: contact.id, ...(contact.email ? { email: contact.email } : {}), accountId: contact.accountId });
405
422
  }
406
423
  }
424
+ const resolvedVia = viaDomain && viaEmail ? "both" : viaDomain ? "account_domain" : "contact_email";
425
+ const via = { resolvedVia, ...(matchedContacts.length ? { matchedContacts } : {}) } as const;
407
426
  if (accountIds.size === 0) {
408
427
  return {
409
428
  dealId: null,
@@ -432,8 +451,9 @@ export function suggestCallDeal(
432
451
  dealName: top.name,
433
452
  accountId: top.accountId,
434
453
  accountName: account?.name,
454
+ ...via,
435
455
  confidence: "high",
436
- reason: `"${top.name}" is the only open deal on matched account "${account?.name ?? top.accountId}".`,
456
+ reason: `"${top.name}" is the only open deal on matched account "${account?.name ?? top.accountId}" (via ${resolvedVia}).`,
437
457
  };
438
458
  }
439
459
  return {
@@ -441,8 +461,9 @@ export function suggestCallDeal(
441
461
  dealName: top.name,
442
462
  accountId: top.accountId,
443
463
  accountName: account?.name,
464
+ ...via,
444
465
  confidence: "low",
445
- reason: `${openDeals.length} open deals on matched account(s); "${top.name}" has the most recent activity. Confirm before writing.`,
466
+ reason: `${openDeals.length} open deals on matched account(s); "${top.name}" has the most recent activity (matched via ${resolvedVia}). Confirm before writing.`,
446
467
  };
447
468
  }
448
469