fullstackgtm 0.49.0 → 0.50.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/DATA-FLOWS.md +1 -0
  3. package/README.md +48 -0
  4. package/dist/acquireCheckpoint.d.ts +33 -0
  5. package/dist/acquireCheckpoint.js +149 -0
  6. package/dist/acquireLinkedIn.d.ts +2 -0
  7. package/dist/acquireLinkedIn.js +1 -1
  8. package/dist/cli/audit.js +6 -1
  9. package/dist/cli/auth.js +25 -0
  10. package/dist/cli/backfill.js +4 -0
  11. package/dist/cli/call.js +28 -6
  12. package/dist/cli/draft.js +11 -1
  13. package/dist/cli/enrich.js +286 -81
  14. package/dist/cli/fix.js +6 -3
  15. package/dist/cli/help.js +30 -26
  16. package/dist/cli/icp.js +15 -1
  17. package/dist/cli/market.js +14 -1
  18. package/dist/cli/planOutput.d.ts +5 -0
  19. package/dist/cli/planOutput.js +27 -0
  20. package/dist/cli/plans.js +269 -24
  21. package/dist/cli/schedule.js +22 -11
  22. package/dist/cli/shared.d.ts +3 -1
  23. package/dist/cli/shared.js +2 -2
  24. package/dist/cli/signals.js +22 -3
  25. package/dist/cli/tam.js +10 -1
  26. package/dist/cli/ui.d.ts +10 -5
  27. package/dist/cli/ui.js +28 -9
  28. package/dist/connectors/hubspot.d.ts +4 -0
  29. package/dist/connectors/hubspot.js +36 -18
  30. package/dist/connectors/linkedin.d.ts +2 -0
  31. package/dist/connectors/linkedin.js +5 -0
  32. package/dist/connectors/prospectSources.d.ts +23 -0
  33. package/dist/connectors/prospectSources.js +21 -3
  34. package/dist/enrich.d.ts +44 -1
  35. package/dist/hostedAcquireCheckpoint.d.ts +43 -0
  36. package/dist/hostedAcquireCheckpoint.js +129 -0
  37. package/dist/hostedPatchPlan.d.ts +87 -0
  38. package/dist/hostedPatchPlan.js +275 -0
  39. package/dist/icp.js +13 -2
  40. package/dist/index.d.ts +4 -1
  41. package/dist/index.js +3 -0
  42. package/dist/planStore.d.ts +14 -0
  43. package/dist/planStore.js +73 -0
  44. package/dist/progress.d.ts +2 -2
  45. package/dist/progress.js +2 -2
  46. package/docs/api.md +24 -0
  47. package/docs/architecture.md +9 -0
  48. package/package.json +1 -1
  49. package/skills/fullstackgtm/SKILL.md +1 -1
  50. package/src/acquireCheckpoint.ts +186 -0
  51. package/src/acquireLinkedIn.ts +3 -1
  52. package/src/cli/audit.ts +5 -1
  53. package/src/cli/auth.ts +24 -0
  54. package/src/cli/backfill.ts +3 -0
  55. package/src/cli/call.ts +25 -6
  56. package/src/cli/draft.ts +9 -1
  57. package/src/cli/enrich.ts +325 -80
  58. package/src/cli/fix.ts +5 -3
  59. package/src/cli/help.ts +31 -26
  60. package/src/cli/icp.ts +13 -1
  61. package/src/cli/market.ts +12 -1
  62. package/src/cli/planOutput.ts +30 -0
  63. package/src/cli/plans.ts +259 -25
  64. package/src/cli/schedule.ts +21 -15
  65. package/src/cli/shared.ts +2 -1
  66. package/src/cli/signals.ts +22 -3
  67. package/src/cli/tam.ts +8 -1
  68. package/src/cli/ui.ts +31 -13
  69. package/src/connectors/hubspot.ts +41 -17
  70. package/src/connectors/linkedin.ts +6 -0
  71. package/src/connectors/prospectSources.ts +46 -4
  72. package/src/enrich.ts +47 -1
  73. package/src/hostedAcquireCheckpoint.ts +156 -0
  74. package/src/hostedPatchPlan.ts +291 -0
  75. package/src/icp.ts +14 -2
  76. package/src/index.ts +20 -0
  77. package/src/planStore.ts +87 -0
  78. package/src/progress.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,83 @@ 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.50.1] — 2026-07-10
11
+
12
+ ### Added
13
+
14
+ - Decision-first CLI presentation across plan previews, plan queues, apply
15
+ outcomes, doctor, schedules, signals, ICP, market, TAM, and call analysis.
16
+ Human defaults use compact cards and ranked summaries; `--verbose` restores
17
+ full audit detail and `--json` retains the stable machine contract.
18
+
19
+ ### Fixed
20
+
21
+ - HubSpot 429/5xx responses now retry with bounded backoff, honor
22
+ `Retry-After`, and report accurate live throttle feedback.
23
+ - Multi-line progress boards no longer scroll intermediate paint frames into
24
+ terminal history.
25
+ - `plans sync` reconciles historical workspaces concurrently with visible
26
+ progress, skips absent legacy mirrors, and avoids echoing hosted-origin
27
+ receipts back to hosted.
28
+ - CLI apply reports only the exact approved receipt subset to hosted while
29
+ retaining excluded rows in the complete local audit trail.
30
+ - Plan and apply output now distinguish approved, applied, excluded, skipped,
31
+ and failed operations without exposing the immutable document's stale
32
+ creation-time status as the current lifecycle state.
33
+
34
+ ## [0.50.0] — 2026-07-10
35
+
36
+ ### Added
37
+
38
+ - CLI and hosted now act as local-first replicas of one immutable patch plan.
39
+ Either surface may execute when it has a compatible CRM connection; online
40
+ execution uses a shared claim, while offline CLI work remains available and
41
+ reconciles later through stable provider idempotency and exact receipts.
42
+ - `fullstackgtm plans sync` explicitly exchanges approval revisions, terminal
43
+ state, and per-operation execution receipts. Plan list, show, and apply also
44
+ perform best-effort ambient check-ins.
45
+ - Hosted can execute fully synchronized CLI-origin plans through its connected
46
+ HubSpot or Salesforce connector. The CLI imports hosted runs without replaying
47
+ provider writes, and hosted imports exact CLI outcomes rather than inferring
48
+ success for every approved row.
49
+
50
+ ### Security
51
+
52
+ - Shared execution claims are bound to the immutable plan, exact approved
53
+ subset, replica, and paired CLI token. Approval edits freeze during apply;
54
+ stranded claims require an explicit audited recovery, and preflight release
55
+ is permitted only before provider I/O by the owning replica.
56
+ - Hosted ingestion recomputes the immutable document digest and verifies an
57
+ exact one-to-one operation set. Terminal transitions require complete,
58
+ duplicate-free receipts for the approved subset and cannot widen authority.
59
+
60
+ ### Fixed
61
+
62
+ - Paired CLIs now mirror saved plans into the hosted Patch Plans queue and
63
+ print an exact review deep-link. Mirrors are org-scoped, immutable and
64
+ idempotent; hosted approvals are hash/operation-verified and locally
65
+ HMAC-signed before CLI execution. Security state and evidence/findings stay
66
+ local, while bounded provider receipts replicate between capable executors.
67
+ - `enrich acquire` now reports discovery, resolution, and plan-building progress
68
+ across the previously silent provider/dedupe middle. Paired hosted runs render
69
+ the same live stage, counters, and generic progress notes on their detail page.
70
+ - Interactive acquisition runs retain both the CRM snapshot and acquisition
71
+ checklists in terminal history, then print a compact plan summary, copyable
72
+ review/approve/apply commands, and the paired hosted Runs deep-link.
73
+ - Persistent progress completion freezes the already-painted final frame rather
74
+ than repainting it, preventing duplicate live/final checklists in scrollback.
75
+ - Acquisition now persists query-bound Pipe0 cursors and HeyReach/Explorium
76
+ offsets so scheduled runs traverse the full audience instead of repeatedly
77
+ exhausting the first 25 results. `--max` controls desired new leads and the
78
+ new `--scan-limit` bounds raw duplicate-heavy scanning. Run history records
79
+ the complete discovery funnel and provider-confirmed exhaustion state.
80
+ - Checkpoints now live in a dedicated profile-scoped store keyed independently
81
+ by provider/source/list/query. Paired CLIs synchronize opaque continuation to
82
+ an organization-scoped hosted record using broker authentication and
83
+ compare-and-swap revisions; conflicts retain the newer hosted checkpoint.
84
+ - The documented LinkedIn `--list <id>` flag is accepted by strict CLI parsing,
85
+ and NAICS-only software ICPs retain their industry constraint on Pipe0.
86
+
10
87
  ## [0.49.0] — 2026-07-10
11
88
 
12
89
  ### Security
package/DATA-FLOWS.md CHANGED
@@ -26,6 +26,7 @@ is no fullstackgtm-operated server in the data path for the open package.**
26
26
  | `enrich append --source apollo`, `enrich refresh` | **Apollo** (api.apollo.io) | The company domain / contact email being enriched | Your `APOLLO_API_KEY` (BYO) |
27
27
  | `market capture`, `market refresh` | **Public vendor websites** you list in `market.config.json` | A bounded HTTP GET; DNS answers are validated and pinned, mixed/private/reserved answers are refused, and every redirect is revalidated | none |
28
28
  | `login --via <url>` (optional) | **Your hosted deployment's broker** | A pairing handshake; the broker mints short-lived CRM tokens | broker pairing token |
29
+ | paired `enrich acquire` checkpoint sync (optional) | **Your hosted deployment** | Opaque query fingerprint plus provider cursor/list offset, exhaustion flag, and CAS revision. No prospect records, emails, names, or provider credentials. Scoped to the broker token's organization. | broker pairing token |
29
30
 
30
31
  Commands not listed (`plans`, `rules`, `doctor`, `schedule`, `audit-log`,
31
32
  `diff`, `merge`, report rendering) make **no network calls**.
package/README.md CHANGED
@@ -192,6 +192,7 @@ echo "$PIPE0_API_KEY" | fullstackgtm login pipe0 # discovery + work-e
192
192
 
193
193
  fullstackgtm enrich acquire --provider hubspot # zero-config: ICP → discover → score → dedup → dry-run diff
194
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
195
196
  fullstackgtm plans approve <id> --operations all
196
197
  fullstackgtm apply --plan-id <id> --provider hubspot # the only step that creates contacts
197
198
  ```
@@ -206,6 +207,53 @@ The **ICP** (`icp.json`) is the single targeting artifact: it generates each pro
206
207
 
207
208
  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.
208
209
 
210
+ Discovery continuation is profile-scoped and independently keyed by the exact
211
+ provider, source, list, and ICP query. Scheduled runs resume the saved Pipe0
212
+ cursor or HeyReach offset instead of rereading page one, even when several
213
+ lists alternate. When the CLI is paired, this non-PII checkpoint is mirrored
214
+ to the hosted organization with compare-and-swap so another authenticated
215
+ worker can resume without moving a newer cursor backwards. `--max` is the desired net-new plan size;
216
+ `--scan-limit` independently bounds raw candidates scanned after ICP filtering
217
+ and dedupe. `enrich status --runs` exposes the full funnel and whether the
218
+ audience is continuing or provider-confirmed exhausted.
219
+
220
+ When paired, every saved plan is also mirrored into the hosted Patch Plans
221
+ review queue and the CLI prints its exact deep-link. The mirror is immutable
222
+ and organization-scoped: replaying the same plan is idempotent, while the same
223
+ id with different content is rejected. Hosted reviewers can approve or reject;
224
+ the plan is executable from any synchronized surface with a compatible CRM
225
+ connection. A CLI-created plan can be applied in hosted, and a hosted approval
226
+ can be applied from the CLI—the plan's origin does not permanently own it.
227
+
228
+ This is local-first replication, not remote control. The local plan store works
229
+ offline, and plan commands perform a best-effort hosted check-in. Run
230
+ `fullstackgtm plans sync` to explicitly exchange every plan's latest approval,
231
+ execution claim, status, and receipts. If hosted applies while the CLI is
232
+ offline, the next check-in imports the exact per-operation results and marks
233
+ the local plan applied. If the CLI applies while hosted is unavailable, it
234
+ keeps the result locally and uploads the receipt on a later check-in.
235
+
236
+ Before either surface writes, it verifies the immutable plan hash, selected
237
+ operation ids, current approval revision, connector capability, and provider
238
+ state. Online replicas use a short-lived execution claim to coordinate; stable
239
+ operation ids, resolve-before-create, compare-and-set guards, and provider
240
+ readback protect offline retries. Receipts record each operation as `applied`,
241
+ `skipped`, or `failed`, plus provider record ids when available, so another
242
+ replica learns what happened rather than inferring it from plan status. A
243
+ conflicting plan body or approval revision is surfaced for review and is never
244
+ merged by expanding authority.
245
+
246
+ Interactive command output is decision-first by default: compact cards show
247
+ status, selected scope, expected effect, per-record outcomes, hosted links, and
248
+ the next safe command. Use `--verbose` on supported human-facing commands for
249
+ the complete findings/evidence/payload document; use `--json` for the stable
250
+ machine-readable contract. Progress and warnings remain on stderr, while JSON
251
+ and requested report data remain clean on stdout.
252
+
253
+ Local signing keys and HMAC digests never upload. Operation before/after values
254
+ do upload to the paired organization because they are required for informed
255
+ review and hosted execution.
256
+
209
257
  **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:
210
258
 
211
259
  ```bash
@@ -0,0 +1,33 @@
1
+ /** The complete identity of one independently traversable provider audience. */
2
+ export type AcquireCheckpointKey = {
3
+ provider: string;
4
+ source: string;
5
+ listId: string | null;
6
+ queryFingerprint: string;
7
+ };
8
+ export type AcquireContinuation = {
9
+ cursor?: string | null;
10
+ offset?: number | null;
11
+ exhausted: boolean;
12
+ };
13
+ /** Versioned wire format shared by local persistence and hosted sync. */
14
+ export type AcquireCheckpoint = {
15
+ version: 1;
16
+ id: string;
17
+ key: AcquireCheckpointKey;
18
+ continuation: AcquireContinuation;
19
+ updatedAt: string;
20
+ };
21
+ export declare function validateAcquireCheckpointKey(value: unknown): AcquireCheckpointKey;
22
+ /** Stable, non-secret filename/key suitable for both filesystem and hosted records. */
23
+ export declare function acquireCheckpointId(key: AcquireCheckpointKey): string;
24
+ export declare function validateAcquireCheckpoint(value: unknown): AcquireCheckpoint;
25
+ export declare function acquireCheckpointsDir(baseDir?: string): string;
26
+ export interface AcquireCheckpointStore {
27
+ get(key: AcquireCheckpointKey): Promise<AcquireCheckpoint | null>;
28
+ put(key: AcquireCheckpointKey, continuation: AcquireContinuation, updatedAt?: Date): Promise<AcquireCheckpoint>;
29
+ /** Import a validated local/hosted record without changing its timestamp. */
30
+ putRecord(record: AcquireCheckpoint): Promise<AcquireCheckpoint>;
31
+ list(): Promise<AcquireCheckpoint[]>;
32
+ }
33
+ export declare function createFileAcquireCheckpointStore(baseDir?: string): AcquireCheckpointStore;
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, mkdirSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { credentialsDir, ensureSecureHomeDir } from "./credentials.js";
5
+ import { assertNoSymlinkComponents, readSecureRegularFile, writeSecureFileAtomic, } from "./secureFile.js";
6
+ const COMPONENT_MAX = 512;
7
+ function requireComponent(value, name) {
8
+ if (typeof value !== "string" || value.length === 0 || value.length > COMPONENT_MAX || /[\u0000-\u001f]/.test(value)) {
9
+ throw new Error(`Invalid acquisition checkpoint ${name}`);
10
+ }
11
+ return value;
12
+ }
13
+ export function validateAcquireCheckpointKey(value) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
15
+ throw new Error("Invalid acquisition checkpoint key");
16
+ }
17
+ const candidate = value;
18
+ const listId = candidate.listId;
19
+ if (listId !== null && listId !== undefined)
20
+ requireComponent(listId, "listId");
21
+ return {
22
+ provider: requireComponent(candidate.provider, "provider"),
23
+ source: requireComponent(candidate.source, "source"),
24
+ listId: listId ?? null,
25
+ queryFingerprint: requireComponent(candidate.queryFingerprint, "queryFingerprint"),
26
+ };
27
+ }
28
+ /** Stable, non-secret filename/key suitable for both filesystem and hosted records. */
29
+ export function acquireCheckpointId(key) {
30
+ const valid = validateAcquireCheckpointKey(key);
31
+ const canonical = JSON.stringify([valid.provider, valid.source, valid.listId, valid.queryFingerprint]);
32
+ return `acqcp_${createHash("sha256").update(canonical).digest("hex")}`;
33
+ }
34
+ function validateContinuation(value) {
35
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
36
+ throw new Error("Invalid acquisition checkpoint continuation");
37
+ }
38
+ const candidate = value;
39
+ if (typeof candidate.exhausted !== "boolean")
40
+ throw new Error("Invalid acquisition checkpoint exhausted flag");
41
+ if (candidate.cursor !== undefined && candidate.cursor !== null)
42
+ requireComponent(candidate.cursor, "cursor");
43
+ if (candidate.offset !== undefined && candidate.offset !== null &&
44
+ (!Number.isSafeInteger(candidate.offset) || candidate.offset < 0)) {
45
+ throw new Error("Invalid acquisition checkpoint offset");
46
+ }
47
+ return {
48
+ ...(candidate.cursor !== undefined ? { cursor: candidate.cursor } : {}),
49
+ ...(candidate.offset !== undefined ? { offset: candidate.offset } : {}),
50
+ exhausted: candidate.exhausted,
51
+ };
52
+ }
53
+ export function validateAcquireCheckpoint(value) {
54
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
55
+ throw new Error("Invalid acquisition checkpoint");
56
+ }
57
+ const candidate = value;
58
+ if (candidate.version !== 1)
59
+ throw new Error(`Unsupported acquisition checkpoint version: ${String(candidate.version)}`);
60
+ const key = validateAcquireCheckpointKey(candidate.key);
61
+ const id = acquireCheckpointId(key);
62
+ if (candidate.id !== id)
63
+ throw new Error("Acquisition checkpoint id does not match its key");
64
+ if (typeof candidate.updatedAt !== "string" || !Number.isFinite(Date.parse(candidate.updatedAt))) {
65
+ throw new Error("Invalid acquisition checkpoint updatedAt");
66
+ }
67
+ return {
68
+ version: 1,
69
+ id,
70
+ key,
71
+ continuation: validateContinuation(candidate.continuation),
72
+ updatedAt: candidate.updatedAt,
73
+ };
74
+ }
75
+ export function acquireCheckpointsDir(baseDir) {
76
+ return join(baseDir ?? credentialsDir(), "acquire", "checkpoints");
77
+ }
78
+ export function createFileAcquireCheckpointStore(baseDir) {
79
+ const directory = acquireCheckpointsDir(baseDir);
80
+ function fileForId(id) {
81
+ return join(directory, `${id}.json`);
82
+ }
83
+ function readId(id) {
84
+ try {
85
+ return validateAcquireCheckpoint(JSON.parse(readSecureRegularFile(fileForId(id), { tightenMode: 0o600 })));
86
+ }
87
+ catch (error) {
88
+ if (error.code === "ENOENT")
89
+ return null;
90
+ throw error;
91
+ }
92
+ }
93
+ function ensureDirectory() {
94
+ if (baseDir === undefined)
95
+ ensureSecureHomeDir();
96
+ // Tighten both managed levels. mkdir's mode does not affect a directory
97
+ // restored or pre-created with broader permissions.
98
+ const levels = [join(baseDir ?? credentialsDir(), "acquire"), directory];
99
+ for (const level of levels) {
100
+ assertNoSymlinkComponents(level);
101
+ mkdirSync(level, { recursive: true, mode: 0o700 });
102
+ assertNoSymlinkComponents(level);
103
+ try {
104
+ chmodSync(level, 0o700);
105
+ }
106
+ catch { /* chmod is unavailable on some filesystems. */ }
107
+ }
108
+ }
109
+ function writeRecord(value) {
110
+ const record = validateAcquireCheckpoint(value);
111
+ ensureDirectory();
112
+ writeSecureFileAtomic(fileForId(record.id), `${JSON.stringify(record, null, 2)}\n`);
113
+ return record;
114
+ }
115
+ return {
116
+ async get(key) {
117
+ return readId(acquireCheckpointId(key));
118
+ },
119
+ async put(key, continuation, updatedAt = new Date()) {
120
+ const validKey = validateAcquireCheckpointKey(key);
121
+ return writeRecord({
122
+ version: 1,
123
+ id: acquireCheckpointId(validKey),
124
+ key: validKey,
125
+ continuation: validateContinuation(continuation),
126
+ updatedAt: updatedAt.toISOString(),
127
+ });
128
+ },
129
+ async putRecord(record) {
130
+ return writeRecord(record);
131
+ },
132
+ async list() {
133
+ let names;
134
+ try {
135
+ assertNoSymlinkComponents(directory);
136
+ names = readdirSync(directory).filter((name) => /^acqcp_[a-f0-9]{64}\.json$/.test(name));
137
+ }
138
+ catch (error) {
139
+ if (error.code === "ENOENT")
140
+ return [];
141
+ throw error;
142
+ }
143
+ return names
144
+ .map((name) => readId(name.slice(0, -5)))
145
+ .filter((record) => record !== null)
146
+ .sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
147
+ },
148
+ };
149
+ }
@@ -25,6 +25,8 @@ export type DiscoverLinkedInOptions = {
25
25
  sourceId?: string;
26
26
  /** Hard cap on prospects pulled. */
27
27
  max?: number;
28
+ /** Opaque provider continuation cursor. */
29
+ cursor?: string;
28
30
  };
29
31
  /**
30
32
  * Discovery branch for `acquireFromApi`: read prospects from a LinkedIn source
@@ -41,7 +41,7 @@ export function linkedInProspectToProspect(lp) {
41
41
  * provider is injected so this is fully testable against the fake (no key).
42
42
  */
43
43
  export async function discoverLinkedInProspects(provider, options = {}) {
44
- const raw = await provider.fetchProspects({ sourceId: options.sourceId, max: options.max });
44
+ const raw = await provider.fetchProspects({ sourceId: options.sourceId, max: options.max, cursor: options.cursor });
45
45
  return raw.map(linkedInProspectToProspect);
46
46
  }
47
47
  /**
package/dist/cli/audit.js CHANGED
@@ -11,6 +11,7 @@ import { auditReportToHtml, auditReportToMarkdown } from "../report.js";
11
11
  import { reportCounts, reportCrm, reportFindings } from "../runReport.js";
12
12
  import { connectorFor, numericOption, option, readSnapshot, saveRequested, selectedRules } from "./shared.js";
13
13
  import { colorEnabled, createChecklist, formatCount, paint, scoreColor, sparkline, stylizePlanMarkdown, table } from "./ui.js";
14
+ import { compactPlan, verbosePlanRequested } from "./planOutput.js";
14
15
  const SEVERITY_RANK = {
15
16
  info: 0,
16
17
  warning: 1,
@@ -241,11 +242,15 @@ export async function audit(args) {
241
242
  if (args.includes("--json")) {
242
243
  console.log(JSON.stringify(plan, null, 2));
243
244
  }
245
+ else if (!verbosePlanRequested(args)) {
246
+ console.log(compactPlan(plan, { saved: saveRequested(args) }));
247
+ console.error(`\n${auditNextStep(args, plan)}`);
248
+ }
244
249
  else {
245
250
  // Default to the summary view (rule table + counts); the full per-operation
246
251
  // dump is opt-in via --full or, for a deliverable, `report`. (dx-punch-list #3)
247
252
  // Interactive terminals get the styled rendering; piped output is unchanged.
248
- console.log(stylizePlanMarkdown(patchPlanToMarkdown(plan, { summary: !args.includes("--full") }), paint(colorEnabled(process.stdout))));
253
+ console.log(stylizePlanMarkdown(patchPlanToMarkdown(plan, { summary: false }), paint(colorEnabled(process.stdout))));
249
254
  console.error(`\n${auditNextStep(args, plan)}`);
250
255
  }
251
256
  if (threshold &&
package/dist/cli/auth.js CHANGED
@@ -579,6 +579,31 @@ export async function doctorCommand(args) {
579
579
  const healthLine = workspace.auditCount === 0
580
580
  ? p.dim("no saved audits yet (fullstackgtm audit --provider <name> --save starts the timeline)")
581
581
  : `${scoreColor(workspace.healthScore ?? 0, p)}/100${delta} — ${workspace.auditCount} audit(s) saved, last ${workspace.lastAuditAt}${healthSparkline(workspace.profile, p.enabled)}`;
582
+ const connectedProviders = Object.entries(report.providers).filter(([, provider]) => provider.source !== "none");
583
+ const blockers = [
584
+ ...(!report.node.ok ? [`Node v${report.node.version} is unsupported; install ${report.node.required}.`] : []),
585
+ ...(connectedProviders.length === 0 ? ["No CRM connected."] : []),
586
+ ...(workspace.pendingPlans.length > 0 ? [`${workspace.pendingPlans.length} plan${workspace.pendingPlans.length === 1 ? "" : "s"} awaiting approval.`] : []),
587
+ ];
588
+ if (!args.includes("--verbose")) {
589
+ const ready = report.node.ok && connectedProviders.length > 0;
590
+ const compact = [
591
+ `${ready ? p.green("Ready") : p.yellow("Setup needed")} · ${report.package.name} ${report.package.version} · profile ${report.profile}`,
592
+ `CRM ${connectedProviders.length > 0 ? connectedProviders.map(([name]) => name).join(", ") : "not connected"}`,
593
+ `Health ${healthLine}`,
594
+ `Plans ${workspace.pendingPlans.length === 0 ? "none awaiting approval" : `${workspace.pendingPlans.length} awaiting approval · ${workspace.pendingPlans[0].id}`}`,
595
+ ...(blockers.length > 0 ? ["", "Attention", ...blockers.map((blocker) => ` ${blocker}`)] : []),
596
+ "",
597
+ "Next",
598
+ ...nextSteps.map((step) => ` ${step}`),
599
+ "",
600
+ "Run `fullstackgtm doctor --verbose` for credential paths and optional tooling checks.",
601
+ ];
602
+ console.log(p.enabled ? box(compact, p, "Workspace readiness").join("\n") : compact.join("\n"));
603
+ if (!report.node.ok)
604
+ process.exitCode = 1;
605
+ return;
606
+ }
582
607
  const lines = [
583
608
  `Package: ${p.bold(`${report.package.name} ${report.package.version}`)}`,
584
609
  `Node: v${report.node.version} (${report.node.required} required) ${mark(report.node.ok)}`,
@@ -14,6 +14,7 @@ import { progressReporter } from "../runReport.js";
14
14
  import { unknownSubcommandError } from "./suggest.js";
15
15
  import { option, readSnapshot, saveRequested } from "./shared.js";
16
16
  import { colorEnabled, createProgressRenderer, paint, stylizePlanMarkdown, table } from "./ui.js";
17
+ import { compactPlan, verbosePlanRequested } from "./planOutput.js";
17
18
  /** STRIPE_SECRET_KEY env ∨ stored `login stripe` credential (same ladder as `--provider stripe`). */
18
19
  function resolveStripeKey() {
19
20
  const key = process.env.STRIPE_SECRET_KEY ?? getCredential("stripe")?.accessToken;
@@ -73,6 +74,9 @@ export async function backfillCommand(args) {
73
74
  if (rest.includes("--json")) {
74
75
  console.log(JSON.stringify({ plan: result.plan, counts: result.counts, unmatched: result.unmatched, proposedAccounts: result.proposedAccounts }, null, 2));
75
76
  }
77
+ else if (!verbosePlanRequested(rest)) {
78
+ console.log(compactPlan(result.plan, { saved: save && result.plan.operations.length > 0 }));
79
+ }
76
80
  else {
77
81
  // TTY-only styling; piped output stays byte-identical plain text.
78
82
  console.log(stylizePlanMarkdown(patchPlanToMarkdown(result.plan), paint(colorEnabled(process.stdout))));
package/dist/cli/call.js CHANGED
@@ -161,11 +161,13 @@ score always needs a key (scoring is LLM work).`);
161
161
  console.log(JSON.stringify(parsed, null, 2));
162
162
  return;
163
163
  }
164
- console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""} (${parsed.sourceSystem})`);
165
- console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights (${parsed.summary.highImportance} high-importance)\n`);
166
- for (const insight of parsed.insights) {
167
- console.log(`[${insight.type}] (importance ${insight.importance}) ${insight.text}`);
168
- }
164
+ console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""}`);
165
+ console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights · ${parsed.summary.highImportance} high priority`);
166
+ const visible = rest.includes("--verbose") ? parsed.insights : parsed.insights.filter((insight) => insight.importance >= 3);
167
+ for (const insight of visible)
168
+ console.log(`\n${insight.type.replaceAll("_", " ")} · priority ${insight.importance}\n ${insight.text.replace(/\s+/g, " ").trim()}`);
169
+ if (!rest.includes("--verbose") && visible.length < parsed.insights.length)
170
+ console.log(`\n${parsed.insights.length - visible.length} lower-priority insight(s) hidden; use --verbose to show all.`);
169
171
  return;
170
172
  }
171
173
  if (subcommand === "link") {
@@ -284,11 +286,31 @@ score always needs a key (scoring is LLM work).`);
284
286
  console.log(JSON.stringify(scorecard, null, 2));
285
287
  return;
286
288
  }
287
- console.log(renderScorecard(scorecard, title));
289
+ console.log(rest.includes("--verbose") ? renderScorecard(scorecard, title) : renderCompactScorecard(scorecard, title));
288
290
  return;
289
291
  }
290
292
  throw new Error(`call supports: parse, classify, link, plan, score (got ${subcommand ?? "nothing"})`);
291
293
  }
294
+ function renderCompactScorecard(scorecard, title) {
295
+ const lines = [
296
+ `${title ?? "Call"} · ${scorecard.overallScore}/${scorecard.scale}${scorecard.band ? ` · ${scorecard.band.label}` : ""}`,
297
+ scorecard.rubricName ? `${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "",
298
+ "",
299
+ ].filter((line, index) => line || index === 2);
300
+ for (const dimension of scorecard.dimensions) {
301
+ lines.push(`${dimension.score}/${dimension.maxScore} ${dimension.name}`);
302
+ lines.push(` ${dimension.coachingNote.replace(/\s+/g, " ").trim()}`);
303
+ }
304
+ if (scorecard.missedItems.length) {
305
+ lines.push("", "Focus next");
306
+ for (const item of scorecard.missedItems)
307
+ lines.push(` ${item}`);
308
+ }
309
+ if (scorecard.highlights.length)
310
+ lines.push("", `Strengths: ${scorecard.highlights.join(" · ")}`);
311
+ lines.push("", "Use --verbose for the full coaching scorecard.");
312
+ return lines.join("\n");
313
+ }
292
314
  function renderScorecard(scorecard, title) {
293
315
  const bandText = scorecard.band ? ` — ${scorecard.band.label}` : "";
294
316
  const rubricLine = scorecard.rubricName ? `Rubric: ${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "";
package/dist/cli/draft.js CHANGED
@@ -7,6 +7,7 @@ import { createFileSignalStore } from "../signals.js";
7
7
  import { createFileJudgeStore } from "../judge.js";
8
8
  import { authorOpeners, DEFAULT_DRAFT_PROMPT, DRAFT_CHANNELS, draft } from "../draft.js";
9
9
  import { numericOption, option, resolveLlmBaseUrls, saveRequested } from "./shared.js";
10
+ import { compactPlan, verbosePlanRequested } from "./planOutput.js";
10
11
  /**
11
12
  * `draft` — author ONE trigger-grounded opener per hot judge decision as a
12
13
  * governed create_task plan. Structurally a proposal: never sends, never writes
@@ -68,7 +69,16 @@ LLM key it emits a clearly-labeled stub rather than fake authored copy.`);
68
69
  channel,
69
70
  openers,
70
71
  });
71
- console.log(JSON.stringify({ drafts, rejected }, null, 2));
72
+ if (args.includes("--json")) {
73
+ console.log(JSON.stringify({ drafts, rejected }, null, 2));
74
+ }
75
+ else if (verbosePlanRequested(args)) {
76
+ console.log(JSON.stringify({ drafts, rejected }, null, 2));
77
+ console.log(compactPlan(plan, { saved: save }));
78
+ }
79
+ else {
80
+ console.log(compactPlan(plan, { saved: save }));
81
+ }
72
82
  const stale = drafts.filter((d) => d.staleTrigger);
73
83
  console.error(`${drafts.length} opener(s) staged as create_task proposals (channel ${channel}, min score ${minScore})` +
74
84
  `${rejected.length ? `; ${rejected.length} rejected (ungrounded first line)` : ""}` +