fullstackgtm 0.53.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.53.1] — 2026-07-11
11
+
12
+ ### Changed
13
+
14
+ - Human-readable `enrich acquire` output shows compact lead cards by default
15
+ before the governed plan preview; `--verbose` remains the full forensic dump.
16
+ - Acquisition budget counters are labeled as applied-create policy headroom,
17
+ explicitly distinguished from market size, and no longer duplicated by a
18
+ second gauge in compact output.
19
+
20
+ ### Fixed
21
+
22
+ - Multi-line progress renderers reserve their terminal rows before painting,
23
+ preventing partial spinner frames from scrolling into final output.
24
+
10
25
  ## [0.53.0] — 2026-07-11
11
26
 
12
27
  ### Added
@@ -23,7 +23,7 @@ import { isSpoolPath, readSpoolPath } from "../spoolFiles.js";
23
23
  import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.js";
24
24
  import { providerKey } from "./tam.js";
25
25
  import { unknownSubcommandError } from "./suggest.js";
26
- import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint } from "./ui.js";
26
+ import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, truncateToWidth } from "./ui.js";
27
27
  import { compactPlan, verbosePlanRequested } from "./planOutput.js";
28
28
  /**
29
29
  * The enrich layer: governed append/refresh of third-party data (Apollo pull,
@@ -785,8 +785,8 @@ function acquireGaugeLine(headroom, budget, p) {
785
785
  const painter = fraction >= 0.9 ? p.red : fraction >= 0.7 ? p.yellow : p.green;
786
786
  parts.push(`${p.dim(label)} ${painter(formatBar(fraction, 10))} ${fmt(used)}/${fmt(cap)}`);
787
787
  };
788
- segment("records/day", headroom.records.day, budget.records?.perDay, String);
789
- segment("records/mo", headroom.records.month, budget.records?.perMonth, String);
788
+ segment("applied/day", headroom.records.day, budget.records?.perDay, String);
789
+ segment("applied/mo", headroom.records.month, budget.records?.perMonth, String);
790
790
  segment("spend/day", headroom.spendUsd.day, budget.spend?.perDay, (value) => `$${value.toFixed(2)}`);
791
791
  segment("spend/mo", headroom.spendUsd.month, budget.spend?.perMonth, (value) => `$${value.toFixed(2)}`);
792
792
  return parts.length > 0 ? parts.join(" · ") : null;
@@ -795,10 +795,33 @@ function formatAcquireMeter(headroom, costPerRecord) {
795
795
  const n = (v) => (v === null ? "∞" : String(v));
796
796
  const money = (v) => (v === null ? "∞" : `$${v.toFixed(2)}`);
797
797
  const max = headroom.maxRecords === null ? "unlimited" : `${headroom.maxRecords}`;
798
- return (`Acquire metercreatable now: ${max} lead(s). ` +
799
- `Records left: ${n(headroom.records.day)}/day, ${n(headroom.records.month)}/month · ` +
800
- `Spend left: ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
801
- `(≈$${costPerRecord.toFixed(2)}/lead).`);
798
+ return `Apply budget (not market size) up to ${max} more create(s) may be applied now; ` +
799
+ `${n(headroom.records.day)} left today, ${n(headroom.records.month)} this month · ` +
800
+ `spend headroom ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
801
+ `(≈$${costPerRecord.toFixed(2)}/lead). Dry runs do not consume this budget.`;
802
+ }
803
+ function renderAcquireLeadCards(result) {
804
+ const p = paint(colorEnabled(process.stdout));
805
+ const evidence = new Map((result.plan.evidence ?? []).map((item) => [item.id, item]));
806
+ return result.plan.operations.map((operation, index) => {
807
+ const payload = operation.afterValue;
808
+ const props = payload.properties ?? {};
809
+ const name = [props.firstname, props.lastname].filter(Boolean).join(" ") || payload.matchValue;
810
+ const title = props.jobtitle || "Title unavailable";
811
+ const company = props.company || payload.associateCompanyName || "Company unavailable";
812
+ let fit;
813
+ const item = operation.evidenceIds?.[0] ? evidence.get(operation.evidenceIds[0]) : undefined;
814
+ try {
815
+ fit = Number(JSON.parse(item?.text ?? "{}").fitScore);
816
+ }
817
+ catch { /* malformed evidence remains display-only */ }
818
+ const lines = [
819
+ truncateToWidth(name, 84),
820
+ truncateToWidth(`${title} · ${company}`, 84),
821
+ truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84),
822
+ ];
823
+ return box(lines, p, `Lead ${index + 1}/${result.plan.operations.length}${Number.isFinite(fit) ? ` · ${Math.round((fit ?? 0) * 100)}% fit` : ""}`).join("\n");
824
+ }).join("\n");
802
825
  }
803
826
  function printAcquireOutput(options) {
804
827
  const { args, result, meter, meterLine, gaugeLine, saved, planSaved, hostedPlanUrl } = options;
@@ -827,15 +850,13 @@ function printAcquireOutput(options) {
827
850
  console.log(` Plan ${hostedPlanUrl}`);
828
851
  }
829
852
  else if (planSaved || !saved) {
853
+ if (result.plan.operations.length > 0)
854
+ console.log(renderAcquireLeadCards(result));
830
855
  console.log(compactPlan(result.plan, { saved: planSaved }));
831
856
  console.log(meterLine);
832
- if (gaugeLine)
833
- console.log(gaugeLine);
834
857
  }
835
858
  else {
836
859
  console.log(meterLine);
837
- if (gaugeLine)
838
- console.log(gaugeLine);
839
860
  console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
840
861
  }
841
862
  if (planSaved) {
package/dist/cli/ui.js CHANGED
@@ -266,6 +266,12 @@ export function createChecklist(items, stream = process.stderr, env = process.en
266
266
  // newline on every repaint can scroll the old top row into permanent
267
267
  // history when the board sits at the bottom of the terminal, producing
268
268
  // apparent duplicate/errored frames in terminal captures.
269
+ // Reserve the board's rows before the first paint. Writing populated rows
270
+ // directly at the terminal bottom can scroll the first rows into history,
271
+ // after which cursor-up repaints target the wrong lines and leave fragments.
272
+ if (painted === 0 && lines.length > 1) {
273
+ stream.write(`${"\n".repeat(lines.length - 1)}\u001b[${lines.length - 1}A`);
274
+ }
269
275
  const up = painted > 1 ? `\u001b[${painted - 1}A` : "";
270
276
  stream.write(`${up}${lines.map((line) => `\u001b[2K${line}`).join("\n")}`);
271
277
  painted = lines.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.53.0",
3
+ "version": "0.53.1",
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)",
package/src/cli/enrich.ts CHANGED
@@ -20,12 +20,12 @@ import { ACQUIRE_STAGES, composeListeners, createProgressEmitter } from "../prog
20
20
  import { createLinkedInProvider, discoverLinkedInProspects } from "../acquireLinkedIn.ts";
21
21
  import { clayPeopleFilterRoutes, fitThreshold, icpToClayPeopleFilters, icpToCrustdataFilters, icpToExploriumFilters, scoreProspectAgainstIcp, type Icp } from "../icp.ts";
22
22
  import { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords, type ApolloPullKey } from "../enrichApollo.ts";
23
- import type { CanonicalGtmSnapshot } from "../types.ts";
23
+ import type { CanonicalGtmSnapshot, CreateRecordPayload } from "../types.ts";
24
24
  import { isSpoolPath, readSpoolPath } from "../spoolFiles.ts";
25
25
  import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveRequested } from "./shared.ts";
26
26
  import { providerKey } from "./tam.ts";
27
27
  import { unknownSubcommandError } from "./suggest.ts";
28
- import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, type Paint } from "./ui.ts";
28
+ import { box, colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, truncateToWidth, type Paint } from "./ui.ts";
29
29
  import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
30
30
  import type { AcquireBudget } from "../acquireMeter.ts";
31
31
 
@@ -903,8 +903,8 @@ function acquireGaugeLine(headroom: AcquireRemaining, budget: AcquireBudget, p:
903
903
  const painter = fraction >= 0.9 ? p.red : fraction >= 0.7 ? p.yellow : p.green;
904
904
  parts.push(`${p.dim(label)} ${painter(formatBar(fraction, 10))} ${fmt(used)}/${fmt(cap)}`);
905
905
  };
906
- segment("records/day", headroom.records.day, budget.records?.perDay, String);
907
- segment("records/mo", headroom.records.month, budget.records?.perMonth, String);
906
+ segment("applied/day", headroom.records.day, budget.records?.perDay, String);
907
+ segment("applied/mo", headroom.records.month, budget.records?.perMonth, String);
908
908
  segment("spend/day", headroom.spendUsd.day, budget.spend?.perDay, (value) => `$${value.toFixed(2)}`);
909
909
  segment("spend/mo", headroom.spendUsd.month, budget.spend?.perMonth, (value) => `$${value.toFixed(2)}`);
910
910
  return parts.length > 0 ? parts.join(" · ") : null;
@@ -914,12 +914,31 @@ function formatAcquireMeter(headroom: AcquireRemaining, costPerRecord: number):
914
914
  const n = (v: number | null) => (v === null ? "∞" : String(v));
915
915
  const money = (v: number | null) => (v === null ? "∞" : `$${v.toFixed(2)}`);
916
916
  const max = headroom.maxRecords === null ? "unlimited" : `${headroom.maxRecords}`;
917
- return (
918
- `Acquire meter creatable now: ${max} lead(s). ` +
919
- `Records left: ${n(headroom.records.day)}/day, ${n(headroom.records.month)}/month · ` +
920
- `Spend left: ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
921
- `(≈$${costPerRecord.toFixed(2)}/lead).`
922
- );
917
+ return `Apply budget (not market size) — up to ${max} more create(s) may be applied now; ` +
918
+ `${n(headroom.records.day)} left today, ${n(headroom.records.month)} this month · ` +
919
+ `spend headroom ${money(headroom.spendUsd.day)}/day, ${money(headroom.spendUsd.month)}/month ` +
920
+ `(≈$${costPerRecord.toFixed(2)}/lead). Dry runs do not consume this budget.`;
921
+ }
922
+
923
+ function renderAcquireLeadCards(result: ReturnType<typeof buildAcquirePlan>): string {
924
+ const p = paint(colorEnabled(process.stdout));
925
+ const evidence = new Map((result.plan.evidence ?? []).map((item) => [item.id, item]));
926
+ return result.plan.operations.map((operation, index) => {
927
+ const payload = operation.afterValue as CreateRecordPayload;
928
+ const props = payload.properties ?? {};
929
+ const name = [props.firstname, props.lastname].filter(Boolean).join(" ") || payload.matchValue;
930
+ const title = props.jobtitle || "Title unavailable";
931
+ const company = props.company || payload.associateCompanyName || "Company unavailable";
932
+ let fit: number | undefined;
933
+ const item = operation.evidenceIds?.[0] ? evidence.get(operation.evidenceIds[0]) : undefined;
934
+ try { fit = Number((JSON.parse(item?.text ?? "{}") as { fitScore?: unknown }).fitScore); } catch { /* malformed evidence remains display-only */ }
935
+ const lines = [
936
+ truncateToWidth(name, 84),
937
+ truncateToWidth(`${title} · ${company}`, 84),
938
+ truncateToWidth([payload.associateCompanyDomain, props.hs_linkedin_url].filter(Boolean).join(" · "), 84),
939
+ ];
940
+ return box(lines, p, `Lead ${index + 1}/${result.plan.operations.length}${Number.isFinite(fit) ? ` · ${Math.round((fit ?? 0) * 100)}% fit` : ""}`).join("\n");
941
+ }).join("\n");
923
942
  }
924
943
 
925
944
  function printAcquireOutput(options: {
@@ -959,12 +978,11 @@ function printAcquireOutput(options: {
959
978
  console.log(` Budget ${gaugeLine ?? meterLine}`);
960
979
  if (hostedPlanUrl) console.log(` Plan ${hostedPlanUrl}`);
961
980
  } else if (planSaved || !saved) {
981
+ if (result.plan.operations.length > 0) console.log(renderAcquireLeadCards(result));
962
982
  console.log(compactPlan(result.plan, { saved: planSaved }));
963
983
  console.log(meterLine);
964
- if (gaugeLine) console.log(gaugeLine);
965
984
  } else {
966
985
  console.log(meterLine);
967
- if (gaugeLine) console.log(gaugeLine);
968
986
  console.log("No net-new leads to create (everything sourced already matched, or was withheld by the meter).");
969
987
  }
970
988
 
package/src/cli/ui.ts CHANGED
@@ -328,6 +328,12 @@ export function createChecklist(
328
328
  // newline on every repaint can scroll the old top row into permanent
329
329
  // history when the board sits at the bottom of the terminal, producing
330
330
  // apparent duplicate/errored frames in terminal captures.
331
+ // Reserve the board's rows before the first paint. Writing populated rows
332
+ // directly at the terminal bottom can scroll the first rows into history,
333
+ // after which cursor-up repaints target the wrong lines and leave fragments.
334
+ if (painted === 0 && lines.length > 1) {
335
+ stream.write(`${"\n".repeat(lines.length - 1)}\u001b[${lines.length - 1}A`);
336
+ }
331
337
  const up = painted > 1 ? `\u001b[${painted - 1}A` : "";
332
338
  stream.write(`${up}${lines.map((line) => `\u001b[2K${line}`).join("\n")}`);
333
339
  painted = lines.length;