fullstackgtm 0.47.0 → 0.48.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 (52) hide show
  1. package/CHANGELOG.md +38 -6
  2. package/INSTALL_FOR_AGENTS.md +3 -3
  3. package/README.md +16 -8
  4. package/dist/audit.js +7 -0
  5. package/dist/backfill.js +2 -16
  6. package/dist/cli/fix.d.ts +3 -0
  7. package/dist/cli/fix.js +70 -1
  8. package/dist/cli/help.d.ts +6 -0
  9. package/dist/cli/help.js +76 -2
  10. package/dist/cli/suggest.d.ts +2 -1
  11. package/dist/cli/suggest.js +49 -3
  12. package/dist/cli.js +40 -15
  13. package/dist/connectors/hubspot.d.ts +2 -1
  14. package/dist/connectors/salesforce.d.ts +2 -1
  15. package/dist/connectors/signalSources.js +2 -11
  16. package/dist/format.js +31 -5
  17. package/dist/freeEmailDomains.d.ts +1 -0
  18. package/dist/freeEmailDomains.js +14 -0
  19. package/dist/hierarchy.d.ts +29 -0
  20. package/dist/hierarchy.js +164 -0
  21. package/dist/index.d.ts +6 -2
  22. package/dist/index.js +5 -1
  23. package/dist/mcp.js +19 -15
  24. package/dist/relationships.d.ts +39 -0
  25. package/dist/relationships.js +101 -0
  26. package/dist/route.d.ts +46 -0
  27. package/dist/route.js +228 -0
  28. package/dist/rules.d.ts +1 -0
  29. package/dist/rules.js +172 -12
  30. package/dist/types.d.ts +15 -0
  31. package/docs/api.md +20 -3
  32. package/docs/architecture.md +5 -2
  33. package/package.json +1 -1
  34. package/skills/fullstackgtm/SKILL.md +2 -2
  35. package/src/audit.ts +7 -0
  36. package/src/backfill.ts +2 -17
  37. package/src/cli/fix.ts +68 -1
  38. package/src/cli/help.ts +83 -2
  39. package/src/cli/suggest.ts +58 -4
  40. package/src/cli.ts +38 -13
  41. package/src/connectors/hubspot.ts +3 -1
  42. package/src/connectors/salesforce.ts +3 -1
  43. package/src/connectors/signalSources.ts +2 -12
  44. package/src/format.ts +33 -5
  45. package/src/freeEmailDomains.ts +14 -0
  46. package/src/hierarchy.ts +185 -0
  47. package/src/index.ts +25 -0
  48. package/src/mcp.ts +22 -16
  49. package/src/relationships.ts +115 -0
  50. package/src/route.ts +278 -0
  51. package/src/rules.ts +192 -12
  52. package/src/types.ts +17 -0
package/CHANGELOG.md CHANGED
@@ -3,10 +3,42 @@
3
3
  All notable changes to the `fullstackgtm` package are documented here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project adheres to [Semantic Versioning](https://semver.org/).
6
- The path to 1.0 is planned in the roadmap doc in the repository.
6
+ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.48.0] — 2026-07-08
11
+
12
+ ### Added
13
+
14
+ - Added `GtmAuditRule.emitsOperations` and `isFindingsOnlyRule` so hosts can derive findings-only rule behavior from the package registry instead of duplicating rule IDs.
15
+ - **Route, hierarchy, and relationships open surfaces.** `route leads` builds governed lead-to-account/owner patch plans; `hierarchy report` and `relationships account` provide read-only planning reports for account trees and stakeholder maps.
16
+
17
+ ### Changed
18
+
19
+ - **CLI human path now uses progressive disclosure by default.** Bare
20
+ `fullstackgtm`/`--help` shows a compact lifecycle-grouped command map instead
21
+ of the full flag wall; `help <command>` and `<command> --help` show focused
22
+ per-command guidance; `help --full` (including `help <command> --full`) keeps
23
+ the complete reference available. `audit` now defaults to a summary view with
24
+ `--full` for per-operation detail and prints next-step guidance on stderr for
25
+ human runs while keeping `--json` machine-clean. The dry-run footer now frames
26
+ approval/write safety as invariants rather than prototype copy.
27
+ - **Patch-plan assumptions and open questions are canonical.** Plan exports and
28
+ markdown use `assumptions` plus `openQuestions`; the duplicate
29
+ `decisionPoints` alias was removed.
30
+
31
+ ### Fixed
32
+
33
+ - **Route plan safety and batching.** Owner routing recognizes both canonical
34
+ user ids and provider CRM owner ids, skips name-only account matches as
35
+ ambiguous, avoids self-conflicting sibling preconditions, groups only contacts
36
+ with multiple operations, and uses one shared free-email domain list.
37
+ - **Hierarchy and relationships correctness.** Hierarchy reports dedupe duplicate
38
+ account ids, avoid common public-suffix parent guesses, and rely on a single
39
+ cycle-dedupe pass. Relationship `--domain` selectors normalize domains and
40
+ markdown preserves zero-value deal amounts.
41
+
10
42
  ## [0.47.0] — 2026-07-06
11
43
 
12
44
  ### Added
@@ -117,7 +149,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
117
149
  doctor, rules, dedupe, resolve, snapshot, and error paths) and the public
118
150
  import surface of `cli.ts` is unchanged. Profiling method, opportunity
119
151
  matrix, and honest non-wins are documented in
120
- [docs/perf-notes.md](./docs/perf-notes.md).
152
+ [docs/perf-notes.md](https://github.com/fullstackgtm/core/blob/main/docs/perf-notes.md).
121
153
 
122
154
  ## [0.45.0] — 2026-07-02
123
155
 
@@ -455,7 +487,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
455
487
  Library: `connectors/signalSources.ts` (`listSignalSources`, `getSignalSource`,
456
488
  `fileSource`, `serpapiNewsSource`, `hubspotFormsSource`), `stagedRowToSignal` +
457
489
  `StagedSignalRow` from `signals.ts`. Design:
458
- [docs/spec-connectors-signals-outbound.md](../../docs/spec-connectors-signals-outbound.md)
490
+ [docs/spec-connectors-signals-outbound.md](https://github.com/fullstackgtm/core/blob/main/docs/spec-connectors-signals-outbound.md)
459
491
  (the connector taxonomy + the Phase 2/3 webhook-spool and governed-channel plan).
460
492
  - **Webhook spool format + conventional landing zone (signals Phase 2).**
461
493
  `--connector file` with no path now reads the conventional spool directory
@@ -844,7 +876,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
844
876
  you approve specific operations and run `apply`… These safety invariants are
845
877
  not beta."), matching the README and agent skill.
846
878
 
847
- See [docs/dx-punch-list.md](./docs/dx-punch-list.md) for the audit that drove this.
879
+ See [docs/dx-punch-list.md](https://github.com/fullstackgtm/core/blob/main/docs/dx-punch-list.md) for the audit that drove this.
848
880
 
849
881
  ## [0.34.0] — 2026-06-18
850
882
 
@@ -1935,7 +1967,7 @@ milestones, not a frozen public contract. 0.10.0 continues the 0.x line
1935
1967
  1.2.2 plus the status/docs corrections below. `fullstackgtm@1.2.1` and
1936
1968
  `@1.2.2` were briefly on npm (2026-06-10) and were unpublished the same
1937
1969
  week; those version numbers are permanently burned per npm policy. The real
1938
- 1.0 will be declared via [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md)
1970
+ 1.0 will be declared via [docs/roadmap-to-1.0.md](https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md)
1939
1971
  once the API surface has survived external usage.
1940
1972
 
1941
1973
  ### Changed
@@ -2230,7 +2262,7 @@ stability commitment attached.
2230
2262
 
2231
2263
  - The hosted app's Convex plan tables are the second `PlanStore`
2232
2264
  implementation target; unifying them onto these types is the remaining
2233
- 0.3.x work (see docs/roadmap-to-1.0.md).
2265
+ 0.3.x work (see https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md).
2234
2266
 
2235
2267
  ## [0.2.0] — Unreleased
2236
2268
 
@@ -33,7 +33,7 @@ machine — that is normal and does not block the next step.
33
33
  fullstackgtm audit --demo --json
34
34
  ```
35
35
 
36
- Expect a JSON patch plan with `dryRun: true` and ~80 findings over a generated,
36
+ Expect a JSON patch plan with `dryRun: true` and 110 findings/110 operations over a generated,
37
37
  deliberately messy CRM. Deterministic per seed: `--seed 7` is the default, so
38
38
  two runs produce identical finding and operation ids. Exit code 0.
39
39
 
@@ -131,8 +131,8 @@ the server resolves them from there (peer-dependency semantics) — so this
131
131
  works from inside existing projects too.
132
132
 
133
133
  Tools exposed over stdio — read-only: `fullstackgtm_audit`,
134
- `fullstackgtm_rules`, `fullstackgtm_suggest`, `fullstackgtm_call_parse`,
135
- `fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
134
+ `fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`,
135
+ `fullstackgtm_call_parse`, `fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
136
136
  `fullstackgtm_apply` (requires explicit `approvedOperationIds`),
137
137
  `fullstackgtm_market_observe` (every quoted span is verified against the
138
138
  stored captures before anything is appended).
package/README.md CHANGED
@@ -2,19 +2,21 @@
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
- **Plan/apply for your GTM stack.** An open-source framework for managing disparate go-to-market data spread across third-party systems: a canonical CRM/GTM data model, deterministic hygiene audits, reviewable dry-run patch plans, and approval-gated write-back to providers.
5
+ **Guardrails for AI agents working in your CRM.**
6
6
 
7
- Think `terraform plan` for your CRM: agents and scripts may *read* everything, but every proposed change becomes a typed patch operation object, field, before, after, reason, risk that a human approves before any provider write happens.
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.
8
8
 
9
- 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.
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.
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.
10
12
 
11
- **Status: beta (0.x).** 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. Connectors: HubSpot (read/write), Salesforce (read/write), Stripe (read-only billing).
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).
12
14
 
13
15
  ## Install
14
16
 
15
17
  ```bash
18
+ npx fullstackgtm audit --demo # zero-install try-it path
16
19
  npm install fullstackgtm # library + CLI in a project
17
- npx fullstackgtm audit --demo # or zero-install via npx
18
20
  npm install github:fullstackgtm/core # or straight from this repo (project-local)
19
21
  npx github:fullstackgtm/core audit --demo # zero-install from the repo
20
22
  ```
@@ -315,7 +317,7 @@ fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
315
317
  - `--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.
316
318
  - Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
317
319
 
318
- "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](./evals/crm/leaderboard/RESULTS.md).
320
+ "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).
319
321
 
320
322
  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.
321
323
 
@@ -533,9 +535,9 @@ Or configure any MCP client (Cursor, Claude Desktop, …) with:
533
535
  }
534
536
  ```
535
537
 
536
- Eight tools are exposed over stdio.
538
+ Nine tools are exposed over stdio.
537
539
 
538
- **Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `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).
540
+ **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).
539
541
 
540
542
  **Gated:** `fullstackgtm_apply` (requires explicit `approvedOperationIds`; placeholders still need value overrides) and `fullstackgtm_market_observe` (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).
541
543
 
@@ -547,6 +549,12 @@ Tokens stored via `fullstackgtm login` are picked up automatically — the env v
547
549
  2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
548
550
  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.
549
551
 
552
+ ## License & boundary
553
+
554
+ 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.
555
+
556
+ 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.
557
+
550
558
  ## Development
551
559
 
552
560
  ```bash
package/dist/audit.js CHANGED
@@ -33,6 +33,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
33
33
  const context = { snapshot, policy, index: buildSnapshotIndex(snapshot) };
34
34
  const findings = [];
35
35
  const operations = [];
36
+ const assumptionsById = new Map();
36
37
  for (const rule of rules) {
37
38
  try {
38
39
  onRule?.(rule.id, "start");
@@ -43,6 +44,10 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
43
44
  const result = rule.evaluate(context);
44
45
  findings.push(...result.findings);
45
46
  operations.push(...result.operations);
47
+ for (const assumption of result.assumptions ?? []) {
48
+ if (!assumptionsById.has(assumption.id))
49
+ assumptionsById.set(assumption.id, assumption);
50
+ }
46
51
  try {
47
52
  onRule?.(rule.id, "done", result.findings.length);
48
53
  }
@@ -50,6 +55,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
50
55
  // progress is presentation-only
51
56
  }
52
57
  }
58
+ const assumptions = Array.from(assumptionsById.values());
53
59
  const evidence = buildEvidence(snapshot, findings, policy.today);
54
60
  const pipelineFindings = buildPipelineFindings(findings, operations, evidence, policy.today);
55
61
  linkOperationContext(operations, findings, evidence);
@@ -63,6 +69,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
63
69
  findings,
64
70
  pipelineFindings,
65
71
  evidence,
72
+ assumptions: assumptions.length > 0 ? assumptions : undefined,
66
73
  operations,
67
74
  };
68
75
  }
package/dist/backfill.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * connector re-resolves each invoice id (resolve-first) and creates only on
12
12
  * a confirmed miss.
13
13
  */
14
+ import { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.js";
14
15
  import { normalizeDomain } from "./merge.js";
15
16
  // Mirrors stableHash in rules.ts (FNV-1a); duplicated to keep backfill.ts
16
17
  // importable without pulling the audit engine (the enrich.ts precedent).
@@ -23,21 +24,6 @@ function fnv1a(value) {
23
24
  return (hash >>> 0).toString(16).padStart(8, "0");
24
25
  }
25
26
  export const DEFAULT_BACKFILL_MATCH_PROPERTY = "stripe_invoice_id";
26
- // Freemail domains never become a company domain: stamping "gmail.com" on an
27
- // account (or resolving a company BY gmail.com) would collapse unrelated
28
- // customers onto one record. Mirrors FREE_MAIL in connectors/signalSources.ts
29
- // (duplicated to keep backfill.ts importable without connector code, the
30
- // fnv1a precedent above).
31
- const FREE_MAIL = new Set([
32
- "gmail.com",
33
- "yahoo.com",
34
- "hotmail.com",
35
- "outlook.com",
36
- "icloud.com",
37
- "aol.com",
38
- "proton.me",
39
- "protonmail.com",
40
- ]);
41
27
  /** `Invoice <number || id> — <customer name>` (the plan-time dedupe name). */
42
28
  function backfillDealName(invoice, companyName) {
43
29
  return `Invoice ${invoice.number ?? invoice.id} — ${invoice.customerName ?? companyName}`;
@@ -102,7 +88,7 @@ export function buildStripeBackfillPlan(invoices, snapshot, opts = {}) {
102
88
  let companyDomain = normalizeDomain(account?.domain);
103
89
  let accountIsNew = false;
104
90
  if (!account && createMissingAccounts) {
105
- const usableDomain = domain && !FREE_MAIL.has(domain) ? domain : undefined;
91
+ const usableDomain = domain && !FREE_EMAIL_DOMAINS.has(domain) ? domain : undefined;
106
92
  const name = invoice.customerName?.trim() || usableDomain;
107
93
  if (name) {
108
94
  companyName = name;
package/dist/cli/fix.d.ts CHANGED
@@ -31,3 +31,6 @@ export declare function reassignCommand(args: string[]): Promise<void>;
31
31
  */
32
32
  export declare function fixCommand(args: string[]): Promise<void>;
33
33
  export declare function suggest(args: string[]): Promise<void>;
34
+ export declare function routeCommand(args: string[]): Promise<void>;
35
+ export declare function hierarchyCommand(args: string[]): Promise<void>;
36
+ export declare function relationshipsCommand(args: string[]): Promise<void>;
package/dist/cli/fix.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
2
- import { readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { auditSnapshot, defaultPolicy } from "../audit.js";
5
5
  import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
@@ -7,6 +7,10 @@ import { applyPatchPlan } from "../connector.js";
7
7
  import { patchPlanToMarkdown } from "../format.js";
8
8
  import { createFilePlanStore } from "../planStore.js";
9
9
  import { resolveRecord } from "../resolve.js";
10
+ import { parseAssignmentPolicy } from "../assign.js";
11
+ import { buildLeadRoutePlan } from "../route.js";
12
+ import { accountHierarchyToMarkdown, buildAccountHierarchy } from "../hierarchy.js";
13
+ import { buildRelationshipMap, relationshipMapToMarkdown } from "../relationships.js";
10
14
  import { buildBulkUpdatePlan } from "../bulkUpdate.js";
11
15
  import { buildDedupePlan } from "../dedupe.js";
12
16
  import { buildReassignPlans } from "../reassign.js";
@@ -344,3 +348,68 @@ function snapshotSourceHint(args) {
344
348
  return `--input ${input} `;
345
349
  return "";
346
350
  }
351
+ function parseJsonOrFile(value) {
352
+ const candidate = resolve(process.cwd(), value);
353
+ const text = existsSync(candidate) ? readFileSync(candidate, "utf8") : value;
354
+ return JSON.parse(text);
355
+ }
356
+ export async function routeCommand(args) {
357
+ const [subcommand, ...rest] = args;
358
+ if (subcommand !== "leads") {
359
+ throw new Error("Usage: fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save] [--json] [--out <path>]");
360
+ }
361
+ const match = option(rest, "--match") ?? "domain";
362
+ if (!["domain", "company", "both"].includes(match))
363
+ throw new Error("--match must be domain, company, or both");
364
+ const policyRaw = option(rest, "--policy");
365
+ const snapshot = await readSnapshot(rest);
366
+ const result = buildLeadRoutePlan(snapshot, {
367
+ matchByDomain: match === "domain" || match === "both",
368
+ matchByCompanyName: match === "company" || match === "both",
369
+ inheritAccountOwner: !rest.includes("--no-inherit-owner"),
370
+ reassignExistingOwner: rest.includes("--reassign-owned"),
371
+ assignmentPolicy: policyRaw ? parseAssignmentPolicy(parseJsonOrFile(policyRaw)) : undefined,
372
+ maxOperations: numericOption(rest, "--max-operations"),
373
+ reason: option(rest, "--reason") ?? undefined,
374
+ });
375
+ if (rest.includes("--json")) {
376
+ const out = option(rest, "--out");
377
+ if (out)
378
+ writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(result.plan, null, 2)}\n`);
379
+ if (saveRequested(rest))
380
+ await createFilePlanStore().save(result.plan);
381
+ console.error(`Route dry-run: ${JSON.stringify(result.counts)}`);
382
+ console.log(JSON.stringify({ counts: result.counts, plan: result.plan }, null, 2));
383
+ return;
384
+ }
385
+ console.error(`Route dry-run: ${result.counts.linkOperations} link(s), ${result.counts.ownerOperations} owner assignment(s), ${result.counts.ambiguousAccountMatches} ambiguous match(es).`);
386
+ await emitPlan(result.plan, rest);
387
+ }
388
+ export async function hierarchyCommand(args) {
389
+ const [subcommand, ...rest] = args;
390
+ if (subcommand !== "report")
391
+ throw new Error("Usage: fullstackgtm hierarchy report [source options] [--json|--out <path>]");
392
+ const snapshot = await readSnapshot(rest);
393
+ const report = buildAccountHierarchy(snapshot);
394
+ const out = option(rest, "--out");
395
+ const rendered = rest.includes("--json") ? `${JSON.stringify(report, null, 2)}\n` : `${accountHierarchyToMarkdown(report)}\n`;
396
+ if (out)
397
+ writeFileSync(resolve(process.cwd(), out), rendered);
398
+ console.log(rendered.trimEnd());
399
+ }
400
+ export async function relationshipsCommand(args) {
401
+ const [subcommand, ...rest] = args;
402
+ if (subcommand !== "account")
403
+ throw new Error("Usage: fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]");
404
+ const accountId = option(rest, "--account-id") ?? undefined;
405
+ const domain = option(rest, "--domain") ?? undefined;
406
+ if (!accountId && !domain)
407
+ throw new Error("relationships account needs --account-id <id> or --domain <domain>");
408
+ const snapshot = await readSnapshot(rest);
409
+ const map = buildRelationshipMap(snapshot, { accountId, domain });
410
+ const out = option(rest, "--out");
411
+ const rendered = rest.includes("--json") ? `${JSON.stringify(map, null, 2)}\n` : `${relationshipMapToMarkdown(map)}\n`;
412
+ if (out)
413
+ writeFileSync(resolve(process.cwd(), out), rendered);
414
+ console.log(rendered.trimEnd());
415
+ }
@@ -10,6 +10,12 @@ export type HelpEntry = {
10
10
  };
11
11
  export declare const HELP: Record<string, HelpEntry>;
12
12
  export declare const BESPOKE_HELP: string[];
13
+ export declare const GLOBAL_FLAGS: string[];
14
+ export declare const GLOBAL_SHORT_FLAGS: string[];
15
+ export declare const SOURCE_FLAGS: string[];
16
+ export declare const AUDIT_FLAGS: string[];
17
+ export declare const COMMAND_FLAGS: Record<string, string[]>;
18
+ export declare const FLAGS_WITH_VALUES: Set<string>;
13
19
  export declare function shortUsage(): string;
14
20
  /**
15
21
  * Interactive-terminal styling for the short front door: a dimmed one-line
package/dist/cli/help.js CHANGED
@@ -46,6 +46,12 @@ Usage:
46
46
  fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]
47
47
  the create gate: exit 0 = safe to create, exit 2 = match
48
48
  found (exists/ambiguous) — call before ANY record creation
49
+ fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save|--json|--out <path>]
50
+ lead-to-account matching + owner routing as a governed plan
51
+ fullstackgtm hierarchy report [source options] [--json|--out <path>]
52
+ account hierarchy view from native parents + subdomains
53
+ fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]
54
+ relationship map from contacts, deals, and activity evidence
49
55
  fullstackgtm market init --category <name> start a market map: vendors + claim taxonomy as reviewable config
50
56
  fullstackgtm market capture [--config <path>] [--run <label>]
51
57
  fullstackgtm market classify [--run <label>] [--vendor <id>] [--model m] [--out <path>]
@@ -386,7 +392,28 @@ export const HELP = {
386
392
  detail: "Prevention, not cleanup. Call before ANY record creation — a sync job, webhook, agent, or script. Returns exists/ambiguous/safe_to_create with matches and reasons; exit 2 = do not create.",
387
393
  seeAlso: ["dedupe", "audit"],
388
394
  },
395
+ hierarchy: {
396
+ summary: "account hierarchy report (native parents + subdomain inference)",
397
+ phase: "Prevent",
398
+ synopsis: ["fullstackgtm hierarchy report [source options] [--json|--out <path>]"],
399
+ detail: "Builds a report-only account tree from provider-native parent ids (when present in raw payloads) and deterministic subdomain inference. It surfaces duplicate-domain and ambiguous-parent conflicts instead of guessing writes.",
400
+ seeAlso: ["route", "relationships"],
401
+ },
402
+ relationships: {
403
+ summary: "relationship map for an account from contacts/deals/activity evidence",
404
+ phase: "Prevent",
405
+ synopsis: ["fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]"],
406
+ detail: "Builds a stakeholder map with inferred buyer roles, sentiment from activity subjects, open deals, and missing-role gaps. Read-only evidence surface; no CRM writes.",
407
+ seeAlso: ["call", "route"],
408
+ },
389
409
  // Remediate — governed writes (all produce plans; nothing writes outside approve → apply)
410
+ route: {
411
+ summary: "lead-to-account matching and owner routing plan",
412
+ phase: "Remediate",
413
+ synopsis: ["fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save|--json|--out <path>]"],
414
+ detail: "Matches contact-shaped leads to accounts by domain and/or company name, inherits account owners (or applies an assignment policy), and emits every change as a dry-run patch plan for approve → apply. Ambiguous matches are surfaced, never guessed.",
415
+ seeAlso: ["resolve", "reassign"],
416
+ },
390
417
  fix: {
391
418
  summary: "one-shot composite: audit one rule → suggest → approve → apply",
392
419
  phase: "Remediate",
@@ -559,15 +586,62 @@ export const HELP = {
559
586
  // Verbs that print their own richer multi-subcommand help; runCli routes their
560
587
  // `--help` to themselves, so commandHelp() only renders these via `help <verb>`.
561
588
  export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "bulk-update", "schedule", "signals", "icp", "draft"];
589
+ export const GLOBAL_FLAGS = ["--help", "--full"];
590
+ export const GLOBAL_SHORT_FLAGS = ["-h"];
591
+ export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
592
+ export const AUDIT_FLAGS = ["--config", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
593
+ // Complete per-command flag registry used by runCli's fail-closed flag
594
+ // validation. Keep this next to HELP so focused help, machine capabilities,
595
+ // and the parser safety gate have one command inventory to reconcile against.
596
+ export const COMMAND_FLAGS = {
597
+ init: ["--source", "--provider", "--out", "--force"],
598
+ login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
599
+ logout: [],
600
+ doctor: ["--json"],
601
+ capabilities: ["--json"],
602
+ "robot-docs": [],
603
+ profiles: ["--json"],
604
+ health: ["--json"],
605
+ snapshot: [...SOURCE_FLAGS, "--since", "--out", "--archive"],
606
+ audit: [...SOURCE_FLAGS, ...AUDIT_FLAGS],
607
+ report: [...SOURCE_FLAGS, ...AUDIT_FLAGS, "--plan", "--client", "--title", "--prepared-by", "--format", "--max-examples"],
608
+ diff: ["--before", "--after", "--config", "--stale-days", "--today", "--json", "--fail-on-new-findings"],
609
+ rules: ["--config", "--json"],
610
+ resolve: [...SOURCE_FLAGS, "--name", "--domain", "--email", "--account-id", "--json"],
611
+ hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
612
+ relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
613
+ route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
614
+ fix: [...SOURCE_FLAGS, "--config", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
615
+ "bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
616
+ dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
617
+ reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
618
+ backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--json"],
619
+ enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--stale-days", "--assign-owner", "--objects", "--max", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--json", "--out"],
620
+ call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
621
+ suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
622
+ plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--json"],
623
+ apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
624
+ "audit-log": ["--in", "--out", "--json"],
625
+ merge: ["--input", "--out", "--json"],
626
+ market: ["--category", "--config", "--vendor", "--snapshot", "--task-account", "--task-deal", "--capture-run", "--run", "--prior-run", "--diff", "--from", "--calls", "--anchor", "--min-mentions", "--max-claims", "--promote-lift", "--model", "--format", "--auto", "--unverified", "--save", "--dry-run", "--json", "--out"],
627
+ tam: [...SOURCE_FLAGS, "--source", "--name", "--icp", "--accounts", "--acv", "--acv-basis", "--acv-from-crm", "--deal-period", "--buyers-per-account", "--cross-checks", "--max", "--max-credits", "--usd-per-credit", "--dry-run", "--confirm", "--cron", "--label", "--save", "--json", "--out"],
628
+ icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
629
+ signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
630
+ draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
631
+ schedule: ["--cron", "--label", "--provider", "--trigger", "--runs", "--json"],
632
+ };
633
+ export const FLAGS_WITH_VALUES = new Set([
634
+ "--account", "--account-id", "--accounts", "--acv", "--acv-basis", "--after", "--anchor", "--api-key", "--approve", "--archive", "--assign-owner", "--attendees", "--before", "--bucket", "--buyers-per-account", "--call", "--call-type", "--calls", "--capture-run", "--category", "--channel", "--client", "--client-id", "--client-secret", "--config", "--connector", "--connector-opt", "--contact", "--cron", "--cross-checks", "--deal", "--deal-period", "--diff", "--domain", "--email", "--except-deal-stage", "--format", "--from", "--from-judge", "--golden", "--guard", "--icp", "--in", "--input", "--instance-url", "--keep", "--key", "--keywords", "--label", "--login-url", "--match", "--match-property", "--max", "--max-claims", "--max-credits", "--max-examples", "--max-operations", "--min-accuracy", "--min-confidence", "--min-mentions", "--min-score", "--model", "--name", "--objects", "--operations", "--out", "--pipeline", "--plan", "--plan-id", "--policy", "--port", "--prepared-by", "--prior-run", "--profile", "--promote-lift", "--prompt", "--provider", "--reason", "--require", "--result", "--rubric", "--rule", "--rules", "--run", "--run-label", "--runs", "--scopes", "--seed", "--set", "--signals-from", "--since", "--snapshot", "--source", "--staged-run", "--stale-days", "--status", "--task-account", "--task-deal", "--title", "--to", "--today", "--token", "--token-env", "--touch", "--transcript", "--trigger", "--usd-per-credit", "--value", "--values-from", "--vendor", "--via", "--watchlist", "--where",
635
+ ]);
562
636
  // Lifecycle-grouped front door. One line per verb, organized by the
563
637
  // Prevent→Detect→Remediate→Verify loop so a new user sees ~6 jobs, not 22 verbs.
564
638
  export function shortUsage() {
565
639
  const groups = [
566
640
  ["Get started", ["init"]],
567
641
  ["Setup & health", ["login", "logout", "doctor", "capabilities", "robot-docs", "profiles", "health"]],
568
- ["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules"]],
642
+ ["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules", "hierarchy", "relationships"]],
569
643
  ["Prevent — gate writes", ["resolve"]],
570
- ["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "enrich", "backfill"]],
644
+ ["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "route", "enrich", "backfill"]],
571
645
  ["Calls → evidence", ["call"]],
572
646
  ["Govern — the plan/apply spine", ["suggest", "plans", "apply", "audit-log", "merge"]],
573
647
  ["Market intelligence", ["market", "tam"]],
@@ -10,7 +10,7 @@ export declare function documentedFlags(): Set<string>;
10
10
  export type FlagTypo = {
11
11
  given: string;
12
12
  /** Human-readable correction, e.g. `--json` or `--rules missing-deal-owner`. */
13
- suggestion: string;
13
+ suggestion: string | null;
14
14
  /** The argv tokens that replace `given` in the corrected command. */
15
15
  replacement: string[];
16
16
  };
@@ -29,6 +29,7 @@ export type FlagTypo = {
29
29
  * --jsn` exited 0 and printed markdown where the agent asked for JSON.
30
30
  */
31
31
  export declare function detectFlagTypo(args: string[]): FlagTypo | null;
32
+ export declare function detectUnknownFlag(command: string, args: string[]): FlagTypo | null;
32
33
  /** Nearest known command, derived from the same HELP table. */
33
34
  export declare function suggestCommand(command: string): string | null;
34
35
  /**
@@ -11,7 +11,7 @@
11
11
  // (exit 1) that prints the exact corrected command for the agent to run
12
12
  // itself; this holds uniformly for read and write verbs, so a typo can never
13
13
  // silently change what a write-shaped invocation stages.
14
- import { HELP, usage } from "./help.js";
14
+ import { COMMAND_FLAGS, FLAGS_WITH_VALUES, GLOBAL_FLAGS, GLOBAL_SHORT_FLAGS, HELP, usage, } from "./help.js";
15
15
  export function levenshtein(a, b) {
16
16
  const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
17
17
  for (let i = 0; i <= a.length; i += 1)
@@ -96,6 +96,52 @@ export function detectFlagTypo(args) {
96
96
  }
97
97
  return null;
98
98
  }
99
+ function isFlagShaped(token) {
100
+ return token !== "-" && token.startsWith("-");
101
+ }
102
+ function suggestCommandFlag(unknown, allowed) {
103
+ const candidates = [...allowed].filter((flag) => flag.startsWith("--"));
104
+ const prefix = candidates
105
+ .filter((flag) => unknown.startsWith(`${flag}-`) || flag.startsWith(unknown))
106
+ .sort((a, b) => b.length - a.length)[0];
107
+ if (prefix)
108
+ return prefix;
109
+ return nearest(unknown.toLowerCase().replace(/_/g, "-"), candidates, 3);
110
+ }
111
+ export function detectUnknownFlag(command, args) {
112
+ const commandFlags = COMMAND_FLAGS[command];
113
+ if (!commandFlags)
114
+ return null;
115
+ const allowed = new Set([...GLOBAL_FLAGS, ...commandFlags]);
116
+ for (let index = 0; index < args.length; index += 1) {
117
+ const token = args[index];
118
+ if (token === "--")
119
+ break;
120
+ if (GLOBAL_SHORT_FLAGS.includes(token))
121
+ continue;
122
+ if (!token.startsWith("-"))
123
+ continue;
124
+ if (!token.startsWith("--")) {
125
+ return { given: token, suggestion: null, replacement: [] };
126
+ }
127
+ const equalsIndex = token.indexOf("=");
128
+ const flag = equalsIndex === -1 ? token : token.slice(0, equalsIndex);
129
+ if (allowed.has(flag)) {
130
+ if (equalsIndex !== -1) {
131
+ const value = token.slice(equalsIndex + 1);
132
+ const replacement = value === "" ? [flag] : [flag, value];
133
+ return { given: token, suggestion: replacement.join(" "), replacement };
134
+ }
135
+ if (FLAGS_WITH_VALUES.has(flag) && args[index + 1] !== undefined && !isFlagShaped(args[index + 1])) {
136
+ index += 1;
137
+ }
138
+ continue;
139
+ }
140
+ const suggestion = suggestCommandFlag(flag, allowed);
141
+ return { given: flag, suggestion, replacement: suggestion ? [suggestion] : [] };
142
+ }
143
+ return null;
144
+ }
99
145
  /** Nearest known command, derived from the same HELP table. */
100
146
  export function suggestCommand(command) {
101
147
  if (!command)
@@ -127,8 +173,8 @@ export function unknownFlagEnvelope(command, args, typo) {
127
173
  code: "UNKNOWN_FLAG",
128
174
  message: `Unknown flag: ${typo.given}`,
129
175
  hints: [
130
- `Did you mean: ${typo.suggestion}`,
131
- `Try: ${correctedCommand(command, args, typo)}`,
176
+ ...(typo.suggestion ? [`Did you mean: ${typo.suggestion}`] : []),
177
+ ...(typo.replacement.length > 0 ? [`Try: ${correctedCommand(command, args, typo)}`] : []),
132
178
  ],
133
179
  },
134
180
  };