fullstackgtm 0.47.0 → 0.49.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 (100) hide show
  1. package/CHANGELOG.md +76 -6
  2. package/CONTRIBUTING.md +23 -4
  3. package/DATA-FLOWS.md +7 -2
  4. package/INSTALL_FOR_AGENTS.md +15 -6
  5. package/README.md +28 -9
  6. package/SECURITY.md +27 -3
  7. package/dist/audit.js +7 -0
  8. package/dist/backfill.js +2 -16
  9. package/dist/cli/audit.js +10 -7
  10. package/dist/cli/auth.js +8 -4
  11. package/dist/cli/fix.d.ts +3 -0
  12. package/dist/cli/fix.js +90 -8
  13. package/dist/cli/help.d.ts +6 -0
  14. package/dist/cli/help.js +81 -3
  15. package/dist/cli/market.js +180 -2
  16. package/dist/cli/plans.js +56 -5
  17. package/dist/cli/suggest.d.ts +2 -1
  18. package/dist/cli/suggest.js +49 -3
  19. package/dist/cli/ui.js +2 -0
  20. package/dist/cli.js +41 -16
  21. package/dist/config.d.ts +13 -1
  22. package/dist/config.js +23 -2
  23. package/dist/connectors/hubspot.d.ts +2 -1
  24. package/dist/connectors/prospectSources.js +4 -11
  25. package/dist/connectors/salesforce.d.ts +2 -1
  26. package/dist/connectors/salesforce.js +12 -5
  27. package/dist/connectors/salesforceAuth.d.ts +9 -0
  28. package/dist/connectors/salesforceAuth.js +47 -5
  29. package/dist/connectors/signalSources.js +2 -11
  30. package/dist/connectors/theirstack.d.ts +0 -19
  31. package/dist/connectors/theirstack.js +3 -10
  32. package/dist/credentials.js +36 -26
  33. package/dist/format.js +31 -5
  34. package/dist/freeEmailDomains.d.ts +1 -0
  35. package/dist/freeEmailDomains.js +14 -0
  36. package/dist/hierarchy.d.ts +29 -0
  37. package/dist/hierarchy.js +164 -0
  38. package/dist/index.d.ts +9 -4
  39. package/dist/index.js +8 -3
  40. package/dist/market.d.ts +1 -1
  41. package/dist/market.js +7 -127
  42. package/dist/marketClassify.d.ts +10 -0
  43. package/dist/marketClassify.js +52 -1
  44. package/dist/marketSourcing.js +7 -45
  45. package/dist/mcp.js +86 -130
  46. package/dist/planStore.d.ts +28 -1
  47. package/dist/planStore.js +195 -49
  48. package/dist/providerError.d.ts +21 -0
  49. package/dist/providerError.js +30 -0
  50. package/dist/publicHttp.d.ts +28 -0
  51. package/dist/publicHttp.js +143 -0
  52. package/dist/relationships.d.ts +39 -0
  53. package/dist/relationships.js +101 -0
  54. package/dist/route.d.ts +46 -0
  55. package/dist/route.js +228 -0
  56. package/dist/rules.d.ts +1 -0
  57. package/dist/rules.js +172 -12
  58. package/dist/secureFile.d.ts +15 -0
  59. package/dist/secureFile.js +164 -0
  60. package/dist/types.d.ts +16 -1
  61. package/docs/api.md +49 -5
  62. package/docs/architecture.md +18 -4
  63. package/llms.txt +7 -0
  64. package/package.json +5 -3
  65. package/skills/fullstackgtm/SKILL.md +9 -3
  66. package/src/audit.ts +7 -0
  67. package/src/backfill.ts +2 -17
  68. package/src/cli/audit.ts +10 -7
  69. package/src/cli/auth.ts +8 -4
  70. package/src/cli/fix.ts +96 -8
  71. package/src/cli/help.ts +88 -3
  72. package/src/cli/market.ts +181 -2
  73. package/src/cli/plans.ts +66 -5
  74. package/src/cli/suggest.ts +58 -4
  75. package/src/cli/ui.ts +1 -0
  76. package/src/cli.ts +39 -14
  77. package/src/config.ts +39 -1
  78. package/src/connectors/hubspot.ts +3 -1
  79. package/src/connectors/prospectSources.ts +4 -11
  80. package/src/connectors/salesforce.ts +15 -6
  81. package/src/connectors/salesforceAuth.ts +46 -6
  82. package/src/connectors/signalSources.ts +2 -12
  83. package/src/connectors/theirstack.ts +3 -10
  84. package/src/credentials.ts +47 -28
  85. package/src/format.ts +33 -5
  86. package/src/freeEmailDomains.ts +14 -0
  87. package/src/hierarchy.ts +185 -0
  88. package/src/index.ts +29 -0
  89. package/src/market.ts +7 -111
  90. package/src/marketClassify.ts +61 -1
  91. package/src/marketSourcing.ts +7 -43
  92. package/src/mcp.ts +115 -152
  93. package/src/planStore.ts +235 -57
  94. package/src/providerError.ts +37 -0
  95. package/src/publicHttp.ts +147 -0
  96. package/src/relationships.ts +115 -0
  97. package/src/route.ts +278 -0
  98. package/src/rules.ts +192 -12
  99. package/src/secureFile.ts +169 -0
  100. package/src/types.ts +18 -0
package/dist/cli/fix.js CHANGED
@@ -1,12 +1,17 @@
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
- import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
5
+ import { loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli } from "../config.js";
6
6
  import { applyPatchPlan } from "../connector.js";
7
7
  import { patchPlanToMarkdown } from "../format.js";
8
8
  import { createFilePlanStore } from "../planStore.js";
9
+ import { verifyApprovalDigests } from "../integrity.js";
9
10
  import { resolveRecord } from "../resolve.js";
11
+ import { parseAssignmentPolicy } from "../assign.js";
12
+ import { buildLeadRoutePlan } from "../route.js";
13
+ import { accountHierarchyToMarkdown, buildAccountHierarchy } from "../hierarchy.js";
14
+ import { buildRelationshipMap, relationshipMapToMarkdown } from "../relationships.js";
10
15
  import { buildBulkUpdatePlan } from "../bulkUpdate.js";
11
16
  import { buildDedupePlan } from "../dedupe.js";
12
17
  import { buildReassignPlans } from "../reassign.js";
@@ -193,8 +198,9 @@ export async function fixCommand(args) {
193
198
  throw new Error("--min-confidence must be high or low");
194
199
  }
195
200
  const includeCreates = args.includes("--include-creates");
196
- const loaded = loadConfig(option(args, "--config") ?? undefined);
197
- const configured = await resolveConfiguredRules(loaded);
201
+ const explicitConfig = option(args, "--config") ?? undefined;
202
+ const loaded = loadConfig(explicitConfig);
203
+ const configured = await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig));
198
204
  const rule = configured.find((candidate) => candidate.id === ruleId);
199
205
  if (!rule) {
200
206
  throw new Error(`Unknown rule: ${ruleId}. Available rules: ${configured.map((r) => r.id).join(", ")}`);
@@ -262,22 +268,33 @@ export async function fixCommand(args) {
262
268
  return;
263
269
  }
264
270
  const connector = await connectorFor(provider, args);
271
+ const claimed = await store.claimApply(plan.id, { provider, source: "fix" });
272
+ const { claimId } = claimed;
273
+ const claimedVerification = verifyApprovalDigests(claimed.stored.plan.operations, claimed.stored.approvedOperationIds, claimed.stored.valueOverrides, claimed.stored.approvalDigests);
274
+ if (!claimedVerification.ok) {
275
+ await store.abortApplyPreflight(plan.id, claimId, "Approval integrity verification failed after the claim and before provider I/O.");
276
+ throw new Error(`Refusing to apply plan ${plan.id}: approval changed while acquiring the apply claim.`);
277
+ }
265
278
  // Live apply board on interactive terminals (stderr; inert otherwise); the
266
279
  // same emitter streams heartbeats to the paired hosted app on long runs.
267
280
  const renderer = createProgressRenderer(APPLY_STAGES);
268
281
  const progress = createProgressEmitter(composeListeners(renderer.listener, progressReporter()));
269
282
  let run;
270
283
  try {
271
- run = await applyPatchPlan(connector, plan, {
272
- approvedOperationIds: approvedIds,
273
- valueOverrides: overrides,
284
+ run = await applyPatchPlan(connector, claimed.stored.plan, {
285
+ approvedOperationIds: claimed.stored.approvedOperationIds,
286
+ valueOverrides: claimed.stored.valueOverrides,
274
287
  progress,
275
288
  });
276
289
  }
290
+ catch (error) {
291
+ await store.markApplyUncertain(plan.id, claimId);
292
+ throw error;
293
+ }
277
294
  finally {
278
295
  renderer.done();
279
296
  }
280
- await store.recordRun(plan.id, run);
297
+ await store.recordRun(plan.id, run, claimId);
281
298
  const counts = { applied: 0, conflict: 0, skipped: 0, failed: 0 };
282
299
  for (const result of run.results)
283
300
  counts[result.status] = (counts[result.status] ?? 0) + 1;
@@ -344,3 +361,68 @@ function snapshotSourceHint(args) {
344
361
  return `--input ${input} `;
345
362
  return "";
346
363
  }
364
+ function parseJsonOrFile(value) {
365
+ const candidate = resolve(process.cwd(), value);
366
+ const text = existsSync(candidate) ? readFileSync(candidate, "utf8") : value;
367
+ return JSON.parse(text);
368
+ }
369
+ export async function routeCommand(args) {
370
+ const [subcommand, ...rest] = args;
371
+ if (subcommand !== "leads") {
372
+ 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>]");
373
+ }
374
+ const match = option(rest, "--match") ?? "domain";
375
+ if (!["domain", "company", "both"].includes(match))
376
+ throw new Error("--match must be domain, company, or both");
377
+ const policyRaw = option(rest, "--policy");
378
+ const snapshot = await readSnapshot(rest);
379
+ const result = buildLeadRoutePlan(snapshot, {
380
+ matchByDomain: match === "domain" || match === "both",
381
+ matchByCompanyName: match === "company" || match === "both",
382
+ inheritAccountOwner: !rest.includes("--no-inherit-owner"),
383
+ reassignExistingOwner: rest.includes("--reassign-owned"),
384
+ assignmentPolicy: policyRaw ? parseAssignmentPolicy(parseJsonOrFile(policyRaw)) : undefined,
385
+ maxOperations: numericOption(rest, "--max-operations"),
386
+ reason: option(rest, "--reason") ?? undefined,
387
+ });
388
+ if (rest.includes("--json")) {
389
+ const out = option(rest, "--out");
390
+ if (out)
391
+ writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(result.plan, null, 2)}\n`);
392
+ if (saveRequested(rest))
393
+ await createFilePlanStore().save(result.plan);
394
+ console.error(`Route dry-run: ${JSON.stringify(result.counts)}`);
395
+ console.log(JSON.stringify({ counts: result.counts, plan: result.plan }, null, 2));
396
+ return;
397
+ }
398
+ console.error(`Route dry-run: ${result.counts.linkOperations} link(s), ${result.counts.ownerOperations} owner assignment(s), ${result.counts.ambiguousAccountMatches} ambiguous match(es).`);
399
+ await emitPlan(result.plan, rest);
400
+ }
401
+ export async function hierarchyCommand(args) {
402
+ const [subcommand, ...rest] = args;
403
+ if (subcommand !== "report")
404
+ throw new Error("Usage: fullstackgtm hierarchy report [source options] [--json|--out <path>]");
405
+ const snapshot = await readSnapshot(rest);
406
+ const report = buildAccountHierarchy(snapshot);
407
+ const out = option(rest, "--out");
408
+ const rendered = rest.includes("--json") ? `${JSON.stringify(report, null, 2)}\n` : `${accountHierarchyToMarkdown(report)}\n`;
409
+ if (out)
410
+ writeFileSync(resolve(process.cwd(), out), rendered);
411
+ console.log(rendered.trimEnd());
412
+ }
413
+ export async function relationshipsCommand(args) {
414
+ const [subcommand, ...rest] = args;
415
+ if (subcommand !== "account")
416
+ throw new Error("Usage: fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]");
417
+ const accountId = option(rest, "--account-id") ?? undefined;
418
+ const domain = option(rest, "--domain") ?? undefined;
419
+ if (!accountId && !domain)
420
+ throw new Error("relationships account needs --account-id <id> or --domain <domain>");
421
+ const snapshot = await readSnapshot(rest);
422
+ const map = buildRelationshipMap(snapshot, { accountId, domain });
423
+ const out = option(rest, "--out");
424
+ const rendered = rest.includes("--json") ? `${JSON.stringify(map, null, 2)}\n` : `${relationshipMapToMarkdown(map)}\n`;
425
+ if (out)
426
+ writeFileSync(resolve(process.cwd(), out), rendered);
427
+ console.log(rendered.trimEnd());
428
+ }
@@ -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>]
@@ -173,6 +179,7 @@ Usage:
173
179
  fullstackgtm plans list [--status <s>] | show <id> | reject <id>
174
180
  fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]
175
181
  fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low] [--include-creates]
182
+ fullstackgtm plans recover <id> --acknowledge-uncertain-writes
176
183
  fullstackgtm apply --plan-id <id> --provider <name>
177
184
  fullstackgtm apply --plan-id <id> --channel outbox (render approved openers; transmits nothing)
178
185
  fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [options]
@@ -220,6 +227,8 @@ Audit options:
220
227
  --config <path> Config file (default: ./fullstackgtm.config.json if present)
221
228
  { "policy": {...}, "rules": {"enabled":[],"disabled":[]},
222
229
  "rulePackages": ["./team-rules.mjs"] }
230
+ --allow-plugins Execute rulePackages from an explicit --config path after review
231
+ --no-plugins Ignore rulePackages; use declarative config and built-in rules only
223
232
  --rules <ids> Comma-separated rule ids to run (default: all; see \`rules\`)
224
233
  --json Print the JSON patch plan instead of markdown
225
234
  --out <path> Also write the JSON patch plan to a file
@@ -386,7 +395,28 @@ export const HELP = {
386
395
  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
396
  seeAlso: ["dedupe", "audit"],
388
397
  },
398
+ hierarchy: {
399
+ summary: "account hierarchy report (native parents + subdomain inference)",
400
+ phase: "Prevent",
401
+ synopsis: ["fullstackgtm hierarchy report [source options] [--json|--out <path>]"],
402
+ 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.",
403
+ seeAlso: ["route", "relationships"],
404
+ },
405
+ relationships: {
406
+ summary: "relationship map for an account from contacts/deals/activity evidence",
407
+ phase: "Prevent",
408
+ synopsis: ["fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]"],
409
+ 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.",
410
+ seeAlso: ["call", "route"],
411
+ },
389
412
  // Remediate — governed writes (all produce plans; nothing writes outside approve → apply)
413
+ route: {
414
+ summary: "lead-to-account matching and owner routing plan",
415
+ phase: "Remediate",
416
+ synopsis: ["fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save|--json|--out <path>]"],
417
+ 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.",
418
+ seeAlso: ["resolve", "reassign"],
419
+ },
390
420
  fix: {
391
421
  summary: "one-shot composite: audit one rule → suggest → approve → apply",
392
422
  phase: "Remediate",
@@ -471,6 +501,7 @@ export const HELP = {
471
501
  "fullstackgtm plans list [--status <s>] | show <id> | reject <id>",
472
502
  "fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
473
503
  "fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
504
+ "fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
474
505
  ],
475
506
  detail: "Approval is explicit and per-operation; placeholders need a concrete --value or a suggestion to be approvable.",
476
507
  seeAlso: ["audit", "suggest", "apply"],
@@ -558,16 +589,63 @@ export const HELP = {
558
589
  };
559
590
  // Verbs that print their own richer multi-subcommand help; runCli routes their
560
591
  // `--help` to themselves, so commandHelp() only renders these via `help <verb>`.
561
- export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "bulk-update", "schedule", "signals", "icp", "draft"];
592
+ export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedule", "signals", "icp", "draft"];
593
+ export const GLOBAL_FLAGS = ["--help", "--full"];
594
+ export const GLOBAL_SHORT_FLAGS = ["-h"];
595
+ export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
596
+ export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
597
+ // Complete per-command flag registry used by runCli's fail-closed flag
598
+ // validation. Keep this next to HELP so focused help, machine capabilities,
599
+ // and the parser safety gate have one command inventory to reconcile against.
600
+ export const COMMAND_FLAGS = {
601
+ init: ["--source", "--provider", "--out", "--force"],
602
+ login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
603
+ logout: [],
604
+ doctor: ["--json"],
605
+ capabilities: ["--json"],
606
+ "robot-docs": [],
607
+ profiles: ["--json"],
608
+ health: ["--json"],
609
+ snapshot: [...SOURCE_FLAGS, "--since", "--out", "--archive"],
610
+ audit: [...SOURCE_FLAGS, ...AUDIT_FLAGS],
611
+ report: [...SOURCE_FLAGS, ...AUDIT_FLAGS, "--plan", "--client", "--title", "--prepared-by", "--format", "--max-examples"],
612
+ diff: ["--before", "--after", "--config", "--allow-plugins", "--no-plugins", "--stale-days", "--today", "--json", "--fail-on-new-findings"],
613
+ rules: ["--config", "--allow-plugins", "--no-plugins", "--json"],
614
+ resolve: [...SOURCE_FLAGS, "--name", "--domain", "--email", "--account-id", "--json"],
615
+ hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
616
+ relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
617
+ route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
618
+ fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
619
+ "bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
620
+ dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
621
+ reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
622
+ backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--json"],
623
+ 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"],
624
+ 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"],
625
+ suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
626
+ plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
627
+ apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
628
+ "audit-log": ["--in", "--out", "--json"],
629
+ merge: ["--input", "--out", "--json"],
630
+ 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"],
631
+ 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"],
632
+ icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
633
+ signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
634
+ draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
635
+ schedule: ["--cron", "--label", "--provider", "--trigger", "--runs", "--json"],
636
+ };
637
+ export const FLAGS_WITH_VALUES = new Set([
638
+ "--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",
639
+ ]);
562
640
  // Lifecycle-grouped front door. One line per verb, organized by the
563
641
  // Prevent→Detect→Remediate→Verify loop so a new user sees ~6 jobs, not 22 verbs.
564
642
  export function shortUsage() {
565
643
  const groups = [
566
644
  ["Get started", ["init"]],
567
645
  ["Setup & health", ["login", "logout", "doctor", "capabilities", "robot-docs", "profiles", "health"]],
568
- ["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules"]],
646
+ ["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules", "hierarchy", "relationships"]],
569
647
  ["Prevent — gate writes", ["resolve"]],
570
- ["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "enrich", "backfill"]],
648
+ ["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "route", "enrich", "backfill"]],
571
649
  ["Calls → evidence", ["call"]],
572
650
  ["Govern — the plan/apply spine", ["suggest", "plans", "apply", "audit-log", "merge"]],
573
651
  ["Market intelligence", ["market", "tam"]],
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { createFilePlanStore } from "../planStore.js";
5
- import { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, loadCaptureTexts, loadMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans } from "../market.js";
5
+ import { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, loadCaptureTexts, loadMarketConfig, observationId, starterMarketConfig, validateObservationSet, verifyEvidenceSpans } from "../market.js";
6
6
  import { assessAxes, axesReportToText } from "../marketAxes.js";
7
7
  import { computeDirectives, computeOverlayStats, directivesToPlan, overlayToMarkdown } from "../marketOverlay.js";
8
8
  import { computeScaleIndex, scaleReportToText } from "../marketScale.js";
@@ -24,6 +24,7 @@ import { unknownSubcommandError } from "./suggest.js";
24
24
  */
25
25
  const MARKET_SUBCOMMANDS = [
26
26
  "init", "capture", "classify", "worksheet", "observe", "fronts", "axes", "overlay", "scale", "report", "refresh",
27
+ "schema", "hints", "observe-template", "review",
27
28
  ];
28
29
  export async function marketCommand(args) {
29
30
  const [subcommand, ...rest] = args;
@@ -35,9 +36,13 @@ export async function marketCommand(args) {
35
36
  market init --category <name> [--out <path>] write a starter market.config.json
36
37
  market init --category <name> --auto --vendor <url> [--vendor <url>...] [--anchor <url>] [--max-claims n]
37
38
  LLM-propose vendors + claim taxonomy from seed pages (needs an API key)
39
+ market schema print exact MarketConfig + ObservationSet shapes and a tiny example
38
40
  market capture [--config <path>] [--run <label>]
39
41
  market classify [--run <label>] [--capture-run <label>] [--vendor <id>] [--model m] [--out <path>]
40
42
  market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
43
+ market hints [--vendor <id>] [--capture-run <label>] [--json]
44
+ market observe-template [--run <label>] [--out <path>]
45
+ market review --from <observations.json> [--json]
41
46
  market observe --from <observations.json|sets.jsonl|spool-dir> [--unverified]
42
47
  market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
43
48
  market axes [--config <path>] [--run <label>] [--json]
@@ -78,6 +83,63 @@ one client's category intel never bleeds into another's). Front states are
78
83
  recomputed deterministically on every invocation — never stored.`);
79
84
  return;
80
85
  }
86
+ if (subcommand === "schema") {
87
+ console.log(`MarketConfig shape:
88
+ {
89
+ "category": "string",
90
+ "anchorVendor": "optional vendor id",
91
+ "vendors": [{
92
+ "id": "stable-vendor-id",
93
+ "name": "Vendor Name",
94
+ "urls": { "home": "https://...", "pricing": null, "product": ["https://..."] },
95
+ "aliases": ["optional alternate names"]
96
+ }],
97
+ "claims": [{
98
+ "id": "stable-claim-id",
99
+ "capability": "Specific differentiating capability",
100
+ "icp": "buyer/segment this claim matters to",
101
+ "pricingStructure": "pricing motion implied by the claim, or unknown",
102
+ "definition": "How to judge LOUD vs QUIET vs ABSENT from page text",
103
+ "terms": ["buyer/search terms for deterministic mention matching"]
104
+ }],
105
+ "surfaceRule": "LOUD = hero/primary positioning; QUIET = supported but secondary; ABSENT = no support; UNOBSERVABLE = no usable capture."
106
+ }
107
+
108
+ ObservationSet shape:
109
+ {
110
+ "id": "set_run1",
111
+ "category": "same category as config",
112
+ "runLabel": "run-1",
113
+ "runAt": "2026-06-11T00:00:00.000Z",
114
+ "extractor": "manual|agent:<model>|llm:<provider>:<model>",
115
+ "observations": [{
116
+ "id": "stable observation id (any unique string is accepted)",
117
+ "vendorId": "vendor id from config",
118
+ "claimId": "claim id from config",
119
+ "observedAt": "2026-06-11",
120
+ "intensity": "loud|quiet|absent|unobservable",
121
+ "confidence": "high|medium|low",
122
+ "reason": "one reviewer-facing sentence",
123
+ "evidence": [{
124
+ "id": "ev1",
125
+ "sourceSystem": "web",
126
+ "sourceObjectType": "page",
127
+ "sourceObjectId": "source URL",
128
+ "text": "VERBATIM quote from captured page text",
129
+ "metadata": { "url": "source URL", "captureHash": "hash from worksheet" }
130
+ }]
131
+ }]
132
+ }
133
+
134
+ Rules:
135
+ - exactly one observation per vendor × claim cell
136
+ - loud/quiet require ≥1 verbatim evidence quote
137
+ - absent/unobservable use [] evidence
138
+ - unobservable means no usable captured page exists for the vendor; if pages were captured but the claim is unsupported, use absent
139
+ - submit with: fullstackgtm market observe --from observations.json
140
+ - derive outputs with: fullstackgtm market fronts --json; fullstackgtm market report --format md --out market-report.md`);
141
+ return;
142
+ }
81
143
  if (subcommand === "init") {
82
144
  const category = option(rest, "--category");
83
145
  if (!category)
@@ -148,7 +210,20 @@ recomputed deterministically on every invocation — never stored.`);
148
210
  return;
149
211
  }
150
212
  if (!rest.includes("--unverified")) {
151
- const { textByHash } = loadCaptureTexts(config.category);
213
+ const { entries, textByHash } = loadCaptureTexts(config.category);
214
+ const usableVendorIds = new Set(entries
215
+ .filter((entry) => entry.runLabel === set.runLabel && entry.captureHash && textByHash.has(entry.captureHash))
216
+ .map((entry) => entry.vendorId));
217
+ const unsupportedUnobservable = set.observations.filter((obs) => obs.intensity === "unobservable" && usableVendorIds.has(obs.vendorId));
218
+ if (unsupportedUnobservable.length > 0) {
219
+ console.error(`Rejected: ${unsupportedUnobservable.length} unobservable reading(s) for vendors with usable captures`);
220
+ for (const obs of unsupportedUnobservable.slice(0, 20)) {
221
+ console.error(` - ${obs.vendorId} × ${obs.claimId}: vendor has captured page text; unsupported claims should be absent, not unobservable`);
222
+ }
223
+ console.error("Use unobservable only when no usable captured page exists for that vendor/run. (--unverified skips this gate when captures genuinely live elsewhere.)");
224
+ process.exitCode = 1;
225
+ return;
226
+ }
152
227
  const failures = verifyEvidenceSpans(set.observations, textByHash);
153
228
  if (failures.length > 0) {
154
229
  console.error(`Rejected: ${failures.length} evidence span(s) failed verification against the stored captures`);
@@ -181,6 +256,109 @@ recomputed deterministically on every invocation — never stored.`);
181
256
  }
182
257
  return;
183
258
  }
259
+ if (subcommand === "hints") {
260
+ const vendorFilter = option(rest, "--vendor");
261
+ const vendorIds = vendorFilter ? [vendorFilter] : config.vendors.map((vendor) => vendor.id);
262
+ const worksheets = vendorIds.map((vendorId) => buildWorksheet(config, vendorId, { captureRun: option(rest, "--capture-run") ?? undefined }));
263
+ if (rest.includes("--json")) {
264
+ console.log(JSON.stringify({
265
+ category: config.category,
266
+ captureRun: worksheets[0]?.captureRun ?? null,
267
+ vendors: worksheets.map((worksheet) => ({ vendor: worksheet.vendor, claimHints: worksheet.claimHints })),
268
+ }, null, 2));
269
+ return;
270
+ }
271
+ for (const worksheet of worksheets) {
272
+ console.log(`\n## ${worksheet.vendor.id} — ${worksheet.vendor.name}`);
273
+ if (worksheet.pages.length === 0) {
274
+ console.log("No usable captures: classify every claim as unobservable.");
275
+ continue;
276
+ }
277
+ for (const hint of worksheet.claimHints) {
278
+ console.log(`\n${hint.claimId}`);
279
+ if (hint.matches.length === 0) {
280
+ console.log(" no lexical matches in captured text (usually absent; still inspect pages for synonyms)");
281
+ continue;
282
+ }
283
+ for (const match of hint.matches)
284
+ console.log(` - ${match.term} @ ${match.url}: ${match.quote}`);
285
+ }
286
+ }
287
+ return;
288
+ }
289
+ if (subcommand === "observe-template") {
290
+ const runLabel = option(rest, "--run") ?? "run-1";
291
+ const now = new Date().toISOString();
292
+ const observations = config.vendors.flatMap((vendor) => {
293
+ const worksheet = buildWorksheet(config, vendor.id, { captureRun: runLabel });
294
+ const noPages = worksheet.pages.length === 0;
295
+ return config.claims.map((claim) => ({
296
+ id: observationId(config.category, runLabel, vendor.id, claim.id),
297
+ vendorId: vendor.id,
298
+ claimId: claim.id,
299
+ observedAt: now.slice(0, 10),
300
+ intensity: noPages ? "unobservable" : "absent",
301
+ confidence: "low",
302
+ reason: noPages ? `No usable captures for ${vendor.name}; cannot judge.` : "TODO: inspect worksheet/hints and classify this cell.",
303
+ evidence: [],
304
+ _claimHints: worksheet.claimHints.find((hint) => hint.claimId === claim.id)?.matches ?? [],
305
+ }));
306
+ });
307
+ const set = { id: `set_${runLabel}`, category: config.category, runLabel, runAt: now, extractor: "manual-template", observations };
308
+ const payload = `${JSON.stringify(set, null, 2)}\n`;
309
+ const outPath = option(rest, "--out");
310
+ if (outPath) {
311
+ writeFileSync(resolve(process.cwd(), outPath), payload);
312
+ console.log(`Wrote ${outPath}: ${observations.length} template observations. Fill TODO cells, then run market review --from ${outPath}`);
313
+ }
314
+ else {
315
+ console.log(payload);
316
+ }
317
+ return;
318
+ }
319
+ if (subcommand === "review") {
320
+ const fromPath = option(rest, "--from");
321
+ if (!fromPath)
322
+ throw new Error("market review requires --from <observations.json>");
323
+ const set = JSON.parse(readFileSync(resolve(process.cwd(), fromPath), "utf8"));
324
+ const errors = validateObservationSet(config, set).map((detail) => ({ code: "invalid_observation_set", detail }));
325
+ const warnings = [];
326
+ const { entries, textByHash } = loadCaptureTexts(config.category);
327
+ const usableVendorIds = new Set(entries
328
+ .filter((entry) => entry.runLabel === set.runLabel && entry.captureHash && textByHash.has(entry.captureHash))
329
+ .map((entry) => entry.vendorId));
330
+ for (const obs of set.observations) {
331
+ if (obs.intensity === "unobservable" && usableVendorIds.has(obs.vendorId)) {
332
+ errors.push({ code: "unobservable_with_captures", detail: `${obs.vendorId} × ${obs.claimId}: vendor has captured page text; use absent when unsupported.` });
333
+ }
334
+ if ((obs.intensity === "absent" || obs.intensity === "unobservable") && usableVendorIds.has(obs.vendorId)) {
335
+ const hints = buildWorksheet(config, obs.vendorId, { captureRun: set.runLabel }).claimHints.find((hint) => hint.claimId === obs.claimId)?.matches ?? [];
336
+ if (hints.length > 0) {
337
+ warnings.push({
338
+ code: "support_candidate_marked_absent",
339
+ detail: `${obs.vendorId} × ${obs.claimId}: ${hints.length} candidate snippet(s) matched terms but cell is ${obs.intensity}; verify absent vs quiet. Example: ${hints[0].quote}`,
340
+ });
341
+ }
342
+ }
343
+ }
344
+ for (const failure of verifyEvidenceSpans(set.observations, textByHash)) {
345
+ errors.push({ code: "bad_evidence", detail: `${failure.vendorId} × ${failure.claimId}: ${failure.problem}` });
346
+ }
347
+ const summary = { ok: errors.length === 0, errors, warnings };
348
+ if (rest.includes("--json")) {
349
+ console.log(JSON.stringify(summary, null, 2));
350
+ }
351
+ else {
352
+ console.log(`${summary.ok ? "OK" : "FAILED"}: ${errors.length} error(s), ${warnings.length} warning(s)`);
353
+ for (const err of errors.slice(0, 20))
354
+ console.log(`ERROR ${err.code}: ${err.detail}`);
355
+ for (const warn of warnings.slice(0, 20))
356
+ console.log(`WARN ${warn.code}: ${warn.detail}`);
357
+ }
358
+ if (errors.length > 0)
359
+ process.exitCode = 1;
360
+ return;
361
+ }
184
362
  if (subcommand === "classify") {
185
363
  const credential = await requireLlmCredential("market classify");
186
364
  const vendorFilter = option(rest, "--vendor");
package/dist/cli/plans.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { auditSnapshot, defaultPolicy } from "../audit.js";
5
- import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
5
+ import { loadConfig, mergePolicy, resolveConfiguredRules, rulePackageTrustFromCli } from "../config.js";
6
6
  import { applyPatchPlan } from "../connector.js";
7
7
  import { diffFindings, diffSnapshots, diffToMarkdown } from "../diff.js";
8
8
  import { createChannelConnector } from "../connectors/outboxChannel.js";
@@ -210,6 +210,19 @@ export async function apply(args) {
210
210
  // A channel (e.g. outbox) renders approved ops to a local artifact and
211
211
  // transmits nothing; a CRM provider writes records. Same governed apply path.
212
212
  const connector = channel ? createChannelConnector(channel) : await connectorFor(provider, args);
213
+ let applyClaimId;
214
+ if (planId && store) {
215
+ const claimed = await store.claimApply(planId, { provider: provider ?? `channel:${channel}`, source: "cli" });
216
+ applyClaimId = claimed.claimId;
217
+ const claimedVerification = verifyApprovalDigests(claimed.stored.plan.operations, claimed.stored.approvedOperationIds, claimed.stored.valueOverrides, claimed.stored.approvalDigests);
218
+ if (!claimedVerification.ok) {
219
+ await store.abortApplyPreflight(planId, claimed.claimId, "Approval integrity verification failed after the claim and before provider I/O.");
220
+ throw new Error(`Refusing to apply plan ${planId}: approval changed while acquiring the apply claim.`);
221
+ }
222
+ plan = claimed.stored.plan;
223
+ approvedOperationIds = claimed.stored.approvedOperationIds;
224
+ valueOverrides = claimed.stored.valueOverrides;
225
+ }
213
226
  // Interactive terminals get a live apply board on stderr while the run
214
227
  // executes (preflight → operations → results, with a per-op safety ticker);
215
228
  // piped runs render nothing. Either way the emitter streams heartbeats to
@@ -224,11 +237,18 @@ export async function apply(args) {
224
237
  progress,
225
238
  });
226
239
  }
240
+ catch (error) {
241
+ // Once applyPatchPlan starts, an exception cannot prove that the provider
242
+ // performed no write. Keep the claim fail-closed for operator reconciliation.
243
+ if (planId && store && applyClaimId)
244
+ await store.markApplyUncertain(planId, applyClaimId);
245
+ throw error;
246
+ }
227
247
  finally {
228
248
  renderer.done();
229
249
  }
230
250
  if (planId && store) {
231
- await store.recordRun(planId, run);
251
+ await store.recordRun(planId, run, applyClaimId);
232
252
  }
233
253
  // Charge the acquire meter for the creates that actually landed.
234
254
  if (createOps.length > 0) {
@@ -272,8 +292,9 @@ export async function diffCommand(args) {
272
292
  }
273
293
  const before = JSON.parse(readFileSync(resolve(process.cwd(), beforePath), "utf8"));
274
294
  const after = JSON.parse(readFileSync(resolve(process.cwd(), afterPath), "utf8"));
275
- const loaded = loadConfig(option(args, "--config") ?? undefined);
276
- const rules = selectedRules(args, await resolveConfiguredRules(loaded));
295
+ const explicitConfig = option(args, "--config") ?? undefined;
296
+ const loaded = loadConfig(explicitConfig);
297
+ const rules = selectedRules(args, await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig)));
277
298
  const policy = mergePolicy(defaultPolicy(), loaded?.config);
278
299
  const today = option(args, "--today");
279
300
  if (today)
@@ -397,6 +418,14 @@ export async function plansCommand(args) {
397
418
  console.log(`Status: ${planStatusWord(stored.status, showPaint)}`);
398
419
  console.log(`Approved operations: ${stored.approvedOperationIds.join(", ") || "none"}`);
399
420
  console.log(`Runs: ${stored.runs.length}`);
421
+ if (stored.applyAttempts?.length) {
422
+ console.log("Apply attempts:");
423
+ for (const attempt of stored.applyAttempts) {
424
+ console.log(` ${attempt.id} ${attempt.status} ${attempt.provider} via ${attempt.source} ${attempt.claimedAt}`);
425
+ if (attempt.note)
426
+ console.log(` ${attempt.note}`);
427
+ }
428
+ }
400
429
  console.log("");
401
430
  console.log(stylizePlanMarkdown(patchPlanToMarkdown(stored.plan), showPaint));
402
431
  return;
@@ -452,5 +481,27 @@ export async function plansCommand(args) {
452
481
  console.log(`Rejected ${planId}.`);
453
482
  return;
454
483
  }
455
- throw unknownSubcommandError("plans", subcommand, ["list", "show", "approve", "reject"]);
484
+ if (subcommand === "recover") {
485
+ const planId = rest.find((arg) => !arg.startsWith("--"));
486
+ if (!planId) {
487
+ throw new Error("Usage: fullstackgtm plans recover <planId> --acknowledge-uncertain-writes");
488
+ }
489
+ const stored = await store.get(planId);
490
+ if (!stored)
491
+ throw new Error(`No stored plan with id ${planId}.`);
492
+ if (stored.status !== "applying" || !stored.applyClaim) {
493
+ throw new Error(`Plan ${planId} has no unresolved apply attempt.`);
494
+ }
495
+ const attempt = stored.applyAttempts?.find((entry) => entry.id === stored.applyClaim?.id);
496
+ if (!rest.includes("--acknowledge-uncertain-writes")) {
497
+ throw new Error(`Plan ${planId} has an unresolved ${attempt?.provider ?? "provider"} apply attempt (${stored.applyClaim.id}). ` +
498
+ "Inspect the affected records in the provider; do not replay automatically. If you accept that some writes may already have landed, " +
499
+ `run \`fullstackgtm plans recover ${planId} --acknowledge-uncertain-writes\`. This clears every approval and requires a fresh review and approval before retrying.`);
500
+ }
501
+ await store.recoverApply(planId);
502
+ console.log(`Released ${planId} to needs_approval after acknowledging uncertain provider state. ` +
503
+ `No writes were replayed. Re-audit provider state, review the plan, then approve operations again before any apply.`);
504
+ return;
505
+ }
506
+ throw unknownSubcommandError("plans", subcommand, ["list", "show", "approve", "reject", "recover"]);
456
507
  }
@@ -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
  /**