fullstackgtm 0.49.0 → 0.50.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 (50) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/DATA-FLOWS.md +1 -0
  3. package/README.md +41 -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/enrich.js +283 -79
  9. package/dist/cli/help.js +13 -9
  10. package/dist/cli/plans.js +100 -6
  11. package/dist/cli/shared.d.ts +3 -1
  12. package/dist/cli/shared.js +2 -2
  13. package/dist/cli/ui.d.ts +10 -5
  14. package/dist/cli/ui.js +16 -5
  15. package/dist/connectors/linkedin.d.ts +2 -0
  16. package/dist/connectors/linkedin.js +5 -0
  17. package/dist/connectors/prospectSources.d.ts +23 -0
  18. package/dist/connectors/prospectSources.js +21 -3
  19. package/dist/enrich.d.ts +44 -1
  20. package/dist/hostedAcquireCheckpoint.d.ts +43 -0
  21. package/dist/hostedAcquireCheckpoint.js +129 -0
  22. package/dist/hostedPatchPlan.d.ts +87 -0
  23. package/dist/hostedPatchPlan.js +270 -0
  24. package/dist/icp.js +13 -2
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.js +3 -0
  27. package/dist/planStore.d.ts +14 -0
  28. package/dist/planStore.js +73 -0
  29. package/dist/progress.d.ts +2 -2
  30. package/dist/progress.js +2 -2
  31. package/docs/api.md +24 -0
  32. package/docs/architecture.md +9 -0
  33. package/package.json +1 -1
  34. package/skills/fullstackgtm/SKILL.md +1 -1
  35. package/src/acquireCheckpoint.ts +186 -0
  36. package/src/acquireLinkedIn.ts +3 -1
  37. package/src/cli/enrich.ts +322 -78
  38. package/src/cli/help.ts +14 -9
  39. package/src/cli/plans.ts +95 -6
  40. package/src/cli/shared.ts +2 -1
  41. package/src/cli/ui.ts +18 -9
  42. package/src/connectors/linkedin.ts +6 -0
  43. package/src/connectors/prospectSources.ts +46 -4
  44. package/src/enrich.ts +47 -1
  45. package/src/hostedAcquireCheckpoint.ts +156 -0
  46. package/src/hostedPatchPlan.ts +286 -0
  47. package/src/icp.ts +14 -2
  48. package/src/index.ts +20 -0
  49. package/src/planStore.ts +87 -0
  50. package/src/progress.ts +2 -2
@@ -0,0 +1,87 @@
1
+ import type { PlanStore, StoredPlan } from "./planStore.ts";
2
+ import type { PatchOperation, PatchPlan, PatchPlanRun } from "./types.ts";
3
+ type Options = {
4
+ fetchImpl?: typeof fetch;
5
+ timeoutMs?: number;
6
+ };
7
+ export type HostedPlanState = {
8
+ patchPlanId: string;
9
+ externalPlanId: string;
10
+ documentSha256: string;
11
+ status: string;
12
+ approvedOperationIds: string[];
13
+ valueOverrides: Record<string, unknown>;
14
+ updatedAt: number;
15
+ runRecord?: PatchPlanRun;
16
+ url: string;
17
+ };
18
+ export type HostedPlanResult = {
19
+ status: "unpaired";
20
+ } | {
21
+ status: "missing";
22
+ } | {
23
+ status: "found" | "saved";
24
+ state: HostedPlanState;
25
+ } | {
26
+ status: "conflict";
27
+ reason: string;
28
+ } | {
29
+ status: "unavailable";
30
+ reason: string;
31
+ };
32
+ /** Review document intentionally omits evidence/findings and all mutable store security state. */
33
+ export declare function hostedReviewDocument(plan: PatchPlan): {
34
+ guards?: import("./types.ts").PlanGuard[] | undefined;
35
+ filter?: {
36
+ objectType: "account" | "contact" | "deal";
37
+ where: string[];
38
+ today?: string;
39
+ } | undefined;
40
+ id: string;
41
+ title: string;
42
+ createdAt: string;
43
+ status: string;
44
+ dryRun: boolean;
45
+ summary: string;
46
+ operations: PatchOperation[];
47
+ };
48
+ export declare function hostedPlanDigest(plan: PatchPlan): string;
49
+ export declare function uploadHostedPatchPlan(stored: StoredPlan, options?: Options): Promise<HostedPlanResult>;
50
+ export declare function readHostedPatchPlan(planId: string, options?: Options): Promise<HostedPlanResult>;
51
+ export type HostedReconcileResult = {
52
+ status: "unchanged" | "unpaired" | "missing";
53
+ } | {
54
+ status: "updated";
55
+ stored: StoredPlan;
56
+ remoteStatus: string;
57
+ } | {
58
+ status: "conflict" | "unavailable";
59
+ reason: string;
60
+ };
61
+ /** Pull a peer's newer lifecycle into the local replica without weakening local integrity gates. */
62
+ export declare function reconcileHostedPatchPlan(store: PlanStore, stored: StoredPlan, options?: Options): Promise<HostedReconcileResult>;
63
+ export declare function reportHostedPlanLifecycle(stored: StoredPlan, options?: Options & {
64
+ claimId?: string;
65
+ }): Promise<HostedPlanResult>;
66
+ export type HostedApplyClaimResult = {
67
+ status: "unpaired";
68
+ } | {
69
+ status: "claimed";
70
+ claimId: string;
71
+ expiresAt?: number;
72
+ } | {
73
+ status: "applied";
74
+ } | {
75
+ status: "conflict" | "unavailable";
76
+ reason: string;
77
+ };
78
+ /** Coordinate online replicas before provider I/O; provider idempotency remains the offline fallback. */
79
+ export declare function claimHostedPlanApply(stored: StoredPlan, options?: Options): Promise<HostedApplyClaimResult>;
80
+ /** Release a shared CLI claim only when preflight aborts before provider I/O. */
81
+ export declare function releaseHostedPlanApply(stored: StoredPlan, claimId: string, reason: string, options?: Options): Promise<{
82
+ status: "released" | "unpaired";
83
+ } | {
84
+ status: "conflict" | "unavailable";
85
+ reason: string;
86
+ }>;
87
+ export {};
@@ -0,0 +1,270 @@
1
+ /** Broker-authenticated mirror for locally governed patch plans. */
2
+ import { createHash } from "node:crypto";
3
+ import { getCredential } from "./credentials.js";
4
+ const ENDPOINT = "/api/cli/patch-plan";
5
+ const TIMEOUT_MS = 5000;
6
+ function broker() {
7
+ const value = getCredential("broker");
8
+ if (!value?.baseUrl || !value.accessToken)
9
+ return null;
10
+ try {
11
+ const url = new URL(value.baseUrl);
12
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
13
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
14
+ return null;
15
+ return { baseUrl: url.href.replace(/\/+$/, ""), token: value.accessToken };
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /** Review document intentionally omits evidence/findings and all mutable store security state. */
22
+ export function hostedReviewDocument(plan) {
23
+ return {
24
+ id: plan.id,
25
+ title: plan.title,
26
+ createdAt: plan.createdAt,
27
+ status: "needs_approval",
28
+ dryRun: true,
29
+ summary: plan.summary,
30
+ operations: plan.operations,
31
+ ...(plan.filter ? { filter: plan.filter } : {}),
32
+ ...(plan.guards ? { guards: plan.guards } : {}),
33
+ };
34
+ }
35
+ export function hostedPlanDigest(plan) {
36
+ return createHash("sha256").update(JSON.stringify(hostedReviewDocument(plan))).digest("hex");
37
+ }
38
+ function mapOperation(op) {
39
+ return {
40
+ packageOpId: op.id,
41
+ objectType: op.objectType,
42
+ ...(op.objectId ? { objectId: op.objectId } : {}),
43
+ fieldOrAction: op.field ?? op.operation,
44
+ beforeValue: op.beforeValue ?? null,
45
+ afterValue: op.afterValue ?? null,
46
+ ...(op.reason ? { reason: op.reason } : {}),
47
+ ...(op.sourceRuleOrPolicy ? { sourceRuleOrPolicy: op.sourceRuleOrPolicy } : {}),
48
+ riskLevel: op.riskLevel,
49
+ approvalRequired: op.approvalRequired,
50
+ ...(op.rollback ? { rollback: op.rollback } : {}),
51
+ };
52
+ }
53
+ function parseState(value, baseUrl) {
54
+ if (!value || typeof value !== "object")
55
+ return null;
56
+ const row = value;
57
+ const id = typeof row.patchPlanId === "string" ? row.patchPlanId : null;
58
+ if (!id)
59
+ return null;
60
+ const approvedOperationIds = Array.isArray(row.approvedOperationIds) ? row.approvedOperationIds.filter((id) => typeof id === "string") : [];
61
+ const runRecord = parseRunRecord(row.runRecord, row.operationReceipts, row.externalPlanId, row.updatedAt);
62
+ return {
63
+ patchPlanId: id,
64
+ externalPlanId: typeof row.externalPlanId === "string" ? row.externalPlanId : "",
65
+ documentSha256: typeof row.documentSha256 === "string" ? row.documentSha256 : "",
66
+ status: typeof row.status === "string" ? row.status : "pending_approval",
67
+ approvedOperationIds,
68
+ valueOverrides: row.valueOverrides && typeof row.valueOverrides === "object" && !Array.isArray(row.valueOverrides) ? row.valueOverrides : {},
69
+ updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : Date.now(),
70
+ ...(runRecord ? { runRecord } : {}),
71
+ url: `${baseUrl}/dashboard/patch-plans?plan=${encodeURIComponent(id)}`,
72
+ };
73
+ }
74
+ function parseRunRecord(value, receiptsValue, externalPlanId, updatedAt) {
75
+ const row = value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
76
+ const planId = typeof row?.planId === "string" ? row.planId : typeof externalPlanId === "string" ? externalPlanId : undefined;
77
+ if (!planId)
78
+ return undefined;
79
+ const rawResults = Array.isArray(row?.results) ? row.results : Array.isArray(receiptsValue) ? receiptsValue : [];
80
+ const results = rawResults.flatMap((entry) => {
81
+ if (!entry || typeof entry !== "object")
82
+ return [];
83
+ const receipt = entry;
84
+ const operationId = typeof receipt.operationId === "string" ? receipt.operationId : typeof receipt.packageOpId === "string" ? receipt.packageOpId : undefined;
85
+ const rawStatus = receipt.status;
86
+ const status = rawStatus === "applied" || rawStatus === "failed" || rawStatus === "skipped" || rawStatus === "conflict" ? rawStatus : undefined;
87
+ if (!operationId || !status)
88
+ return [];
89
+ return [{ operationId, status, ...(typeof receipt.detail === "string" ? { detail: receipt.detail } : typeof receipt.error === "string" ? { detail: receipt.error } : {}), ...(receipt.providerData !== undefined ? { providerData: receipt.providerData } : {}) }];
90
+ });
91
+ if (results.length === 0)
92
+ return undefined;
93
+ const timestamp = typeof updatedAt === "number" ? new Date(updatedAt).toISOString() : new Date().toISOString();
94
+ const provider = typeof row?.provider === "string" ? row.provider : "mock";
95
+ const status = row?.status === "applied" || row?.status === "partial" || row?.status === "failed" || row?.status === "rejected"
96
+ ? row.status : results.every((result) => result.status === "applied" || result.status === "skipped") ? "applied" : "partial";
97
+ return {
98
+ planId, provider,
99
+ startedAt: typeof row?.startedAt === "string" ? row.startedAt : timestamp,
100
+ finishedAt: typeof row?.finishedAt === "string" ? row.finishedAt : timestamp,
101
+ status,
102
+ results,
103
+ };
104
+ }
105
+ function failure(status) {
106
+ if (status === 409)
107
+ return { status: "conflict", reason: "hosted mirror has different immutable content" };
108
+ if (status === 401 || status === 403)
109
+ return { status: "unavailable", reason: "hosted authentication was rejected; pair the CLI again" };
110
+ return { status: "unavailable", reason: `hosted plan request failed (HTTP ${status})` };
111
+ }
112
+ export async function uploadHostedPatchPlan(stored, options = {}) {
113
+ const paired = broker();
114
+ if (!paired)
115
+ return { status: "unpaired" };
116
+ const document = hostedReviewDocument(stored.plan);
117
+ const digest = hostedPlanDigest(stored.plan);
118
+ const riskOrder = { low: 0, medium: 1, high: 2 };
119
+ const riskLevel = stored.plan.operations.reduce((risk, op) => riskOrder[op.riskLevel] > riskOrder[risk] ? op.riskLevel : risk, "low");
120
+ try {
121
+ const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}`, {
122
+ method: "POST",
123
+ headers: { Authorization: `Bearer ${paired.token}`, "Content-Type": "application/json" },
124
+ body: JSON.stringify({
125
+ externalPlanId: stored.plan.id, documentSha256: digest, externalRevision: stored.revision,
126
+ title: stored.plan.title, summary: stored.plan.summary, riskLevel, document,
127
+ operations: stored.plan.operations.map(mapOperation), createdAt: Date.parse(stored.plan.createdAt),
128
+ }),
129
+ signal: AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS),
130
+ });
131
+ if (!response.ok)
132
+ return failure(response.status);
133
+ const raw = await response.json();
134
+ const state = parseState({ ...raw, externalPlanId: stored.plan.id, documentSha256: digest, approvedOperationIds: [], valueOverrides: {}, updatedAt: Date.now() }, paired.baseUrl);
135
+ return state ? { status: "saved", state } : { status: "unavailable", reason: "hosted plan response was invalid" };
136
+ }
137
+ catch {
138
+ return { status: "unavailable", reason: "hosted plan request was unavailable" };
139
+ }
140
+ }
141
+ export async function readHostedPatchPlan(planId, options = {}) {
142
+ const paired = broker();
143
+ if (!paired)
144
+ return { status: "unpaired" };
145
+ try {
146
+ const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}?externalPlanId=${encodeURIComponent(planId)}`, {
147
+ headers: { Authorization: `Bearer ${paired.token}`, Accept: "application/json" },
148
+ signal: AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS),
149
+ });
150
+ if (response.status === 404)
151
+ return { status: "missing" };
152
+ if (!response.ok)
153
+ return failure(response.status);
154
+ const state = parseState(await response.json(), paired.baseUrl);
155
+ return state ? { status: "found", state } : { status: "unavailable", reason: "hosted plan response was invalid" };
156
+ }
157
+ catch {
158
+ return { status: "unavailable", reason: "hosted plan request was unavailable" };
159
+ }
160
+ }
161
+ /** Pull a peer's newer lifecycle into the local replica without weakening local integrity gates. */
162
+ export async function reconcileHostedPatchPlan(store, stored, options = {}) {
163
+ const hosted = await readHostedPatchPlan(stored.plan.id, options);
164
+ if (hosted.status === "unpaired" || hosted.status === "missing")
165
+ return { status: hosted.status };
166
+ if (hosted.status === "unavailable" || hosted.status === "conflict")
167
+ return hosted;
168
+ if (hosted.state.documentSha256 !== hostedPlanDigest(stored.plan)) {
169
+ return { status: "conflict", reason: "hosted review content does not match the immutable local plan" };
170
+ }
171
+ if (!["approved", "rejected", "applied"].includes(hosted.state.status))
172
+ return { status: "unchanged" };
173
+ try {
174
+ const reconciled = await store.reconcileReplica(stored.plan.id, {
175
+ status: hosted.state.status,
176
+ approvedOperationIds: hosted.state.approvedOperationIds,
177
+ valueOverrides: hosted.state.valueOverrides,
178
+ ...(hosted.state.runRecord ? { run: hosted.state.runRecord } : {}),
179
+ });
180
+ return reconciled.changed
181
+ ? { status: "updated", stored: reconciled.stored, remoteStatus: hosted.state.status }
182
+ : { status: "unchanged" };
183
+ }
184
+ catch (error) {
185
+ return { status: "conflict", reason: error instanceof Error ? error.message : "replica reconciliation failed" };
186
+ }
187
+ }
188
+ export async function reportHostedPlanLifecycle(stored, options = {}) {
189
+ const paired = broker();
190
+ if (!paired)
191
+ return { status: "unpaired" };
192
+ const status = stored.status === "applied" ? "applied" : stored.status === "rejected" ? "rejected" : "approved";
193
+ const runRecord = status === "applied" ? stored.runs.at(-1) : undefined;
194
+ const operationReceipts = runRecord?.results.map((result) => ({
195
+ packageOpId: result.operationId,
196
+ status: result.status,
197
+ ...(result.detail ? { error: result.detail.slice(0, 1000) } : {}),
198
+ ...(result.providerData !== undefined ? { providerData: result.providerData } : {}),
199
+ updatedAt: Date.parse(runRecord.finishedAt),
200
+ }));
201
+ try {
202
+ const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}`, {
203
+ method: "POST", headers: { Authorization: `Bearer ${paired.token}`, "Content-Type": "application/json" },
204
+ body: JSON.stringify({ action: "lifecycle", externalPlanId: stored.plan.id, documentSha256: hostedPlanDigest(stored.plan), status, approvedOperationIds: stored.approvedOperationIds, ...(options.claimId ? { claimId: options.claimId } : {}), ...(runRecord ? { runRecord, operationReceipts } : {}) }),
205
+ signal: AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS),
206
+ });
207
+ if (!response.ok)
208
+ return failure(response.status);
209
+ const raw = await response.json();
210
+ const state = parseState({ ...raw, externalPlanId: stored.plan.id, documentSha256: hostedPlanDigest(stored.plan), approvedOperationIds: stored.approvedOperationIds, valueOverrides: stored.valueOverrides, updatedAt: Date.now() }, paired.baseUrl);
211
+ return state ? { status: "saved", state } : { status: "unavailable", reason: "hosted lifecycle response was invalid" };
212
+ }
213
+ catch {
214
+ return { status: "unavailable", reason: "hosted lifecycle request was unavailable" };
215
+ }
216
+ }
217
+ /** Coordinate online replicas before provider I/O; provider idempotency remains the offline fallback. */
218
+ export async function claimHostedPlanApply(stored, options = {}) {
219
+ const paired = broker();
220
+ if (!paired)
221
+ return { status: "unpaired" };
222
+ try {
223
+ const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}`, {
224
+ method: "POST", headers: { Authorization: `Bearer ${paired.token}`, "Content-Type": "application/json" },
225
+ body: JSON.stringify({ action: "claim", externalPlanId: stored.plan.id, documentSha256: hostedPlanDigest(stored.plan) }),
226
+ signal: AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS),
227
+ });
228
+ if (!response.ok)
229
+ return failure(response.status);
230
+ const row = await response.json();
231
+ if (row.alreadyApplied === true || row.status === "applied")
232
+ return { status: "applied" };
233
+ if (row.status === "claimed" && typeof row.claimId === "string") {
234
+ return { status: "claimed", claimId: row.claimId, ...(typeof row.expiresAt === "number" ? { expiresAt: row.expiresAt } : {}) };
235
+ }
236
+ return { status: "unavailable", reason: "hosted apply claim response was invalid" };
237
+ }
238
+ catch {
239
+ return { status: "unavailable", reason: "hosted apply claim request was unavailable" };
240
+ }
241
+ }
242
+ /** Release a shared CLI claim only when preflight aborts before provider I/O. */
243
+ export async function releaseHostedPlanApply(stored, claimId, reason, options = {}) {
244
+ const paired = broker();
245
+ if (!paired)
246
+ return { status: "unpaired" };
247
+ try {
248
+ const response = await (options.fetchImpl ?? fetch)(`${paired.baseUrl}${ENDPOINT}`, {
249
+ method: "POST",
250
+ headers: { Authorization: `Bearer ${paired.token}`, "Content-Type": "application/json" },
251
+ body: JSON.stringify({
252
+ action: "release_claim",
253
+ externalPlanId: stored.plan.id,
254
+ documentSha256: hostedPlanDigest(stored.plan),
255
+ claimId,
256
+ reason,
257
+ }),
258
+ signal: AbortSignal.timeout(options.timeoutMs ?? TIMEOUT_MS),
259
+ });
260
+ if (!response.ok)
261
+ return failure(response.status);
262
+ const row = await response.json();
263
+ return row.released === true
264
+ ? { status: "released" }
265
+ : { status: "unavailable", reason: "hosted apply claim release response was invalid" };
266
+ }
267
+ catch {
268
+ return { status: "unavailable", reason: "hosted apply claim release was unavailable" };
269
+ }
270
+ }
package/dist/icp.js CHANGED
@@ -158,6 +158,14 @@ const CRUSTDATA_INDUSTRY = {
158
158
  fintech: ["Financial Services"],
159
159
  "financial services": ["Financial Services"],
160
160
  };
161
+ // Common NAICS groups translated into Crustdata's LinkedIn industry vocabulary.
162
+ // This keeps an ICP authored with provider-neutral NAICS codes from silently
163
+ // dropping its industry constraint on the Pipe0 path.
164
+ const NAICS_CRUSTDATA_INDUSTRY = {
165
+ "5112": ["Software Development", "Computer Software"],
166
+ "5182": ["IT Services and IT Consulting", "Information Technology & Services"],
167
+ "5415": ["IT Services and IT Consulting", "Information Technology & Services"],
168
+ };
161
169
  /**
162
170
  * pipe0 Crustdata people-search config.filters from the ICP. `current_job_titles`
163
171
  * matches real LinkedIn title strings (case-sensitive), so keywords are Title
@@ -185,9 +193,12 @@ export function icpToCrustdataFilters(icp) {
185
193
  if (icp.firmographics.geos?.length) {
186
194
  f.locations = icp.firmographics.geos.map((g) => COUNTRY_NAMES[g.toLowerCase()] ?? g);
187
195
  }
188
- if (icp.firmographics.industries?.length) {
196
+ if (icp.firmographics.industries?.length || icp.firmographics.naics?.length) {
189
197
  const inds = [
190
- ...new Set(icp.firmographics.industries.flatMap((i) => CRUSTDATA_INDUSTRY[i.toLowerCase()] ?? [titleCase(i)])),
198
+ ...new Set([
199
+ ...(icp.firmographics.industries ?? []).flatMap((i) => CRUSTDATA_INDUSTRY[i.toLowerCase()] ?? [titleCase(i)]),
200
+ ...(icp.firmographics.naics ?? []).flatMap((code) => NAICS_CRUSTDATA_INDUSTRY[code] ?? []),
201
+ ]),
191
202
  ];
192
203
  if (inds.length)
193
204
  f.current_employers_linkedin_industries = inds;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { auditSnapshot, defaultPolicy } from "./audit.ts";
2
+ export { acquireCheckpointId, acquireCheckpointsDir, createFileAcquireCheckpointStore, validateAcquireCheckpoint, validateAcquireCheckpointKey, type AcquireCheckpoint, type AcquireCheckpointKey, type AcquireCheckpointStore, type AcquireContinuation, } from "./acquireCheckpoint.ts";
2
3
  export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, type AssignmentContext, type AssignmentPolicy, type AssignmentResult, type AssignmentStrategy, type TerritoryRule, } from "./assign.ts";
3
4
  export { buildBulkUpdatePlan, isFilterableField, parseWhere, type BulkUpdateOptions } from "./bulkUpdate.ts";
4
5
  export { buildDedupePlan, dedupeKey, type DedupeOptions } from "./dedupe.ts";
@@ -17,7 +18,9 @@ export { createStripeConnector, fetchStripePaidInvoices, type StripeConnectorOpt
17
18
  export { buildStripeBackfillPlan, DEFAULT_BACKFILL_MATCH_PROPERTY, type StripeBackfillCounts, type StripeBackfillOptions, type StripeBackfillResult, type StripeBackfillUnmatched, } from "./backfill.ts";
18
19
  export { activeProfile, credentialsDir, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, listProfiles, resolveHubspotAccessToken, resolveHubspotConnection, setActiveProfile, storeCredential, type HubspotConnection, type StoredCredential, } from "./credentials.ts";
19
20
  export { generateDemoSnapshot, type DemoSnapshotOptions } from "./demo.ts";
20
- export { buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, enrichRunId, inferIngestObjectType, ingestKeyValue, latestStamps, loadEnrichConfig, matchSourceRecord, parseCsv, parseEnrichConfig, resolveCrmField, selectStaleWork, sourceValueAt, stagedSourceRecords, staleDaysFor, type BuildEnrichPlanOptions, type EnrichAmbiguity, type EnrichConfig, type EnrichCounts, type EnrichFieldConfig, type EnrichMatchConfig, type EnrichMode, type EnrichObjectType, type EnrichPlanResult, type EnrichRun, type EnrichRunStore, type EnrichSourceConfig, type EnrichSourceRecord, type EnrichStamp, type EnrichWorkItem, type MatchOutcome, } from "./enrich.ts";
21
+ export { buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, enrichRunId, inferIngestObjectType, ingestKeyValue, latestStamps, loadEnrichConfig, matchSourceRecord, parseCsv, parseEnrichConfig, resolveCrmField, selectStaleWork, sourceValueAt, stagedSourceRecords, staleDaysFor, type BuildEnrichPlanOptions, type AcquireDiscoveryCheckpoint, type EnrichAmbiguity, type EnrichConfig, type EnrichCounts, type EnrichFieldConfig, type EnrichMatchConfig, type EnrichMode, type EnrichObjectType, type EnrichPlanResult, type EnrichRun, type EnrichRunStore, type EnrichSourceConfig, type EnrichSourceRecord, type EnrichStamp, type EnrichWorkItem, type MatchOutcome, } from "./enrich.ts";
22
+ export { readHostedAcquireCheckpoint, writeHostedAcquireCheckpoint, type HostedAcquireCheckpoint, type HostedCheckpointReadResult, type HostedCheckpointWriteResult, } from "./hostedAcquireCheckpoint.ts";
23
+ export { claimHostedPlanApply, hostedPlanDigest, hostedReviewDocument, readHostedPatchPlan, reconcileHostedPatchPlan, releaseHostedPlanApply, reportHostedPlanLifecycle, uploadHostedPatchPlan } from "./hostedPatchPlan.ts";
21
24
  export { scaffoldWorkspace, starterEnrichConfig, starterIcp, starterPlaybook, type InitProvider, type InitSource, type ScaffoldFile, type ScaffoldOptions, } from "./init.ts";
22
25
  export { appendCoverage, classifyAccount, classifyCoverage, computeCoverage, coverageCountsFromSnapshot, coveredAccounts, coverageToText, crmCheckableCriteria, deriveAcvFromClosedWon, deriveBuyersPerAccount, estimateTam, loadTamModel, projectEta, readCoverageTimeline, saveTamModel, tamDir, tamReportToMarkdown, type AccountTamClass, type AcvBasis, type DerivedAcv, type DerivedBuyers, type EstimateTamInput, type TamClassified, type TamCoverage, type TamCoverageCounts, type TamCrossCheck, type TamEta, type TamModel, type TamTargeting, type TamUniverse, } from "./tam.ts";
23
26
  export { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords, type ApolloClient, type ApolloClientOptions, type ApolloPullKey, type ApolloPullResult, } from "./enrichApollo.ts";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { auditSnapshot, defaultPolicy } from "./audit.js";
2
+ export { acquireCheckpointId, acquireCheckpointsDir, createFileAcquireCheckpointStore, validateAcquireCheckpoint, validateAcquireCheckpointKey, } from "./acquireCheckpoint.js";
2
3
  export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, } from "./assign.js";
3
4
  export { buildBulkUpdatePlan, isFilterableField, parseWhere } from "./bulkUpdate.js";
4
5
  export { buildDedupePlan, dedupeKey } from "./dedupe.js";
@@ -18,6 +19,8 @@ export { buildStripeBackfillPlan, DEFAULT_BACKFILL_MATCH_PROPERTY, } from "./bac
18
19
  export { activeProfile, credentialsDir, credentialsPath, DEFAULT_PROFILE, deleteCredential, getCredential, listProfiles, resolveHubspotAccessToken, resolveHubspotConnection, setActiveProfile, storeCredential, } from "./credentials.js";
19
20
  export { generateDemoSnapshot } from "./demo.js";
20
21
  export { buildEnrichPlan, createFileEnrichRunStore, DEFAULT_STALE_DAYS, ENRICH_CONFIG_FILE_NAME, enrichRunId, inferIngestObjectType, ingestKeyValue, latestStamps, loadEnrichConfig, matchSourceRecord, parseCsv, parseEnrichConfig, resolveCrmField, selectStaleWork, sourceValueAt, stagedSourceRecords, staleDaysFor, } from "./enrich.js";
22
+ export { readHostedAcquireCheckpoint, writeHostedAcquireCheckpoint, } from "./hostedAcquireCheckpoint.js";
23
+ export { claimHostedPlanApply, hostedPlanDigest, hostedReviewDocument, readHostedPatchPlan, reconcileHostedPatchPlan, releaseHostedPlanApply, reportHostedPlanLifecycle, uploadHostedPatchPlan } from "./hostedPatchPlan.js";
21
24
  export { scaffoldWorkspace, starterEnrichConfig, starterIcp, starterPlaybook, } from "./init.js";
22
25
  export { appendCoverage, classifyAccount, classifyCoverage, computeCoverage, coverageCountsFromSnapshot, coveredAccounts, coverageToText, crmCheckableCriteria, deriveAcvFromClosedWon, deriveBuyersPerAccount, estimateTam, loadTamModel, projectEta, readCoverageTimeline, saveTamModel, tamDir, tamReportToMarkdown, } from "./tam.js";
23
26
  export { apolloPullKeysForAppend, apolloPullKeysForRefresh, createApolloClient, pullApolloRecords, } from "./enrichApollo.js";
@@ -24,6 +24,7 @@ export type StoredPlan = {
24
24
  id: string;
25
25
  claimedAt: string;
26
26
  revision: number;
27
+ hostedClaimId?: string;
27
28
  };
28
29
  /** Durable journal of apply ownership. An unresolved entry must never be replayed automatically. */
29
30
  applyAttempts?: ApplyAttempt[];
@@ -40,6 +41,7 @@ export type ApplyAttempt = {
40
41
  resolvedAt?: string;
41
42
  /** Deliberately generic: provider errors can contain CRM data or credentials. */
42
43
  note?: string;
44
+ hostedClaimId?: string;
43
45
  };
44
46
  export interface PlanStore {
45
47
  save(plan: PatchPlan): Promise<StoredPlan>;
@@ -51,11 +53,23 @@ export interface PlanStore {
51
53
  stored: StoredPlan;
52
54
  claimId: string;
53
55
  }>;
56
+ recordHostedClaim(planId: string, claimId: string, hostedClaimId: string): Promise<StoredPlan>;
54
57
  recordRun(planId: string, run: PatchPlanRun, claimId: string): Promise<StoredPlan>;
55
58
  abortApplyPreflight(planId: string, claimId: string, note: string): Promise<StoredPlan>;
56
59
  markApplyUncertain(planId: string, claimId: string): Promise<StoredPlan>;
57
60
  recoverApply(planId: string): Promise<StoredPlan>;
61
+ reconcileReplica(planId: string, remote: ReplicaPlanState): Promise<{
62
+ stored: StoredPlan;
63
+ changed: boolean;
64
+ }>;
58
65
  }
66
+ /** A validated lifecycle snapshot received from another plan replica. */
67
+ export type ReplicaPlanState = {
68
+ status: "approved" | "rejected" | "applied";
69
+ approvedOperationIds: string[];
70
+ valueOverrides?: Record<string, unknown>;
71
+ run?: PatchPlanRun;
72
+ };
59
73
  /**
60
74
  * Plans as JSON files in a directory (default `$FSGTM_HOME/plans`), one file
61
75
  * per plan id. Filesystem-shaped on purpose: greppable, diffable, and any
package/dist/planStore.js CHANGED
@@ -216,6 +216,19 @@ export function createFilePlanStore(directory) {
216
216
  });
217
217
  });
218
218
  },
219
+ async recordHostedClaim(planId, claimId, hostedClaimId) {
220
+ return withPlanLock(planId, () => {
221
+ const stored = mustRead(planId);
222
+ if (stored.status !== "applying" || stored.applyClaim?.id !== claimId) {
223
+ throw new Error(`Refusing to attach hosted claim for plan ${planId}: local apply claim does not match.`);
224
+ }
225
+ return write({
226
+ ...stored,
227
+ applyClaim: { ...stored.applyClaim, hostedClaimId },
228
+ applyAttempts: (stored.applyAttempts ?? []).map((attempt) => attempt.id === claimId ? { ...attempt, hostedClaimId } : attempt),
229
+ });
230
+ });
231
+ },
219
232
  async abortApplyPreflight(planId, claimId, note) {
220
233
  return withPlanLock(planId, () => {
221
234
  const stored = mustRead(planId);
@@ -266,5 +279,65 @@ export function createFilePlanStore(directory) {
266
279
  });
267
280
  });
268
281
  },
282
+ async reconcileReplica(planId, remote) {
283
+ return withPlanLock(planId, () => {
284
+ const stored = mustRead(planId);
285
+ if (stored.status === "applying" || stored.applyClaim) {
286
+ throw new Error(`Plan ${planId} is applying locally; replica reconciliation will retry after it finishes.`);
287
+ }
288
+ const known = new Set(stored.plan.operations.map((operation) => operation.id));
289
+ const approvedOperationIds = Array.from(new Set(remote.approvedOperationIds));
290
+ if (approvedOperationIds.some((id) => !known.has(id))) {
291
+ throw new Error(`Replica state for ${planId} contains an unknown operation.`);
292
+ }
293
+ // Terminal receipts are monotonic. A delayed approval or rejection can
294
+ // never roll an already-applied local replica backwards.
295
+ if (stored.status === "applied" && remote.status !== "applied") {
296
+ return { stored, changed: false };
297
+ }
298
+ if (remote.status === "rejected") {
299
+ if (stored.status === "rejected")
300
+ return { stored, changed: false };
301
+ return { stored: write({
302
+ ...stored, status: "rejected", approvedOperationIds: [], valueOverrides: {},
303
+ approvalDigests: undefined, applyClaim: undefined,
304
+ }), changed: true };
305
+ }
306
+ if (approvedOperationIds.length === 0) {
307
+ throw new Error(`Replica ${remote.status} state for ${planId} has no approved operations.`);
308
+ }
309
+ const valueOverrides = remote.valueOverrides ?? {};
310
+ const approvalDigests = computeApprovalDigests(stored.plan.operations, approvedOperationIds, valueOverrides, loadOrCreateSigningKey());
311
+ if (remote.status === "approved") {
312
+ const unchanged = stored.status === "approved"
313
+ && JSON.stringify(stored.approvedOperationIds) === JSON.stringify(approvedOperationIds)
314
+ && JSON.stringify(stored.valueOverrides) === JSON.stringify(valueOverrides);
315
+ if (unchanged)
316
+ return { stored, changed: false };
317
+ return { stored: write({
318
+ ...stored, status: "approved", approvedOperationIds, valueOverrides, approvalDigests,
319
+ }), changed: true };
320
+ }
321
+ if (!remote.run || remote.run.planId !== planId) {
322
+ throw new Error(`Replica applied state for ${planId} is missing a matching execution receipt.`);
323
+ }
324
+ if (remote.run.results.some((result) => !known.has(result.operationId))) {
325
+ throw new Error(`Replica execution receipt for ${planId} contains an unknown operation.`);
326
+ }
327
+ const runKey = (run) => `${run.startedAt}\0${run.finishedAt}\0${run.provider}`;
328
+ const alreadyRecorded = stored.runs.some((run) => runKey(run) === runKey(remote.run));
329
+ if (stored.status === "applied" && alreadyRecorded)
330
+ return { stored, changed: false };
331
+ return { stored: write({
332
+ ...stored,
333
+ status: "applied",
334
+ approvedOperationIds,
335
+ valueOverrides,
336
+ approvalDigests,
337
+ applyClaim: undefined,
338
+ runs: alreadyRecorded ? stored.runs : [...stored.runs, remote.run],
339
+ }), changed: true };
340
+ });
341
+ },
269
342
  };
270
343
  }
@@ -89,8 +89,8 @@ export declare const BACKFILL_STRIPE_STAGES: readonly ["invoices", "snapshot", "
89
89
  /** `backfill runs` — replaying local plan runs + health history to the broker. */
90
90
  export declare const RUNS_REPLAY_STAGES: readonly ["runs", "health"];
91
91
  export declare const APPLY_STAGES: readonly ["preflight", "operations", "results"];
92
- /** `enrich acquire` — routing sourced candidate rows into a create plan. */
93
- export declare const ACQUIRE_STAGES: readonly ["candidates"];
92
+ /** `enrich acquire` — discovery through a governed create plan. */
93
+ export declare const ACQUIRE_STAGES: readonly ["discovery", "resolution", "plan"];
94
94
  export declare const MARKET_CAPTURE_STAGES: readonly ["sources", "capture", "classify", "persist"];
95
95
  /** No-op emitter for callers that don't care (keeps signatures simple). */
96
96
  export declare function nullProgressEmitter(): ProgressEmitter;
package/dist/progress.js CHANGED
@@ -133,8 +133,8 @@ export const BACKFILL_STRIPE_STAGES = ["invoices", "snapshot", "matching", "plan
133
133
  /** `backfill runs` — replaying local plan runs + health history to the broker. */
134
134
  export const RUNS_REPLAY_STAGES = ["runs", "health"];
135
135
  export const APPLY_STAGES = ["preflight", "operations", "results"];
136
- /** `enrich acquire` — routing sourced candidate rows into a create plan. */
137
- export const ACQUIRE_STAGES = ["candidates"];
136
+ /** `enrich acquire` — discovery through a governed create plan. */
137
+ export const ACQUIRE_STAGES = ["discovery", "resolution", "plan"];
138
138
  export const MARKET_CAPTURE_STAGES = ["sources", "capture", "classify", "persist"];
139
139
  /** No-op emitter for callers that don't care (keeps signatures simple). */
140
140
  export function nullProgressEmitter() {
package/docs/api.md CHANGED
@@ -229,6 +229,15 @@ webhook landing-zone format (docs/signal-spool-format.md).
229
229
 
230
230
  ## Acquire (net-new lead generation)
231
231
 
232
+ API discovery is continuation-aware. Completed acquire runs persist a
233
+ provider/source/list/query-keyed Pipe0 cursor, Explorium page, or HeyReach list
234
+ offset, plus the discovered → qualified → deduped → resolved → proposed funnel.
235
+ Each list progresses independently. If a broker credential exists, GET/POST
236
+ `/api/cli/acquisition-checkpoint` synchronizes the opaque checkpoint inside the
237
+ token's organization using numeric compare-and-swap revisions; unpaired and
238
+ offline CLIs remain local-first. A changed ICP/list starts a new traversal. CLI
239
+ `--max` sets desired new leads while `--scan-limit` bounds raw candidates.
240
+
232
241
  `buildAcquirePlan` turns sourced-but-unmatched prospects into `create_record`
233
242
  operations (matched / ambiguous are skipped — resolve-first never creates over
234
243
  a possible dup), capped by the meter's headroom. `builtinAcquirePreset` is the
@@ -308,6 +317,21 @@ operator reconciliation remains necessary after a timeout or crash.
308
317
 
309
318
  ## Schedule
310
319
 
320
+ ## Hosted patch-plan mirror
321
+
322
+ `uploadHostedPatchPlan(stored)` mirrors a bounded review document when the
323
+ active profile has a secure broker pairing. `readHostedPatchPlan(planId)` reads
324
+ the org-scoped hosted decision, and `reportHostedPlanLifecycle(stored)` reflects
325
+ local approval/rejection/application. Upload is local-first and best-effort;
326
+ immutable hash conflicts fail closed. Hosted approval does not carry local
327
+ signing material: `apply --plan-id` verifies the hash and operation set before
328
+ `PlanStore.approveOperations` generates machine-local approval digests.
329
+
330
+ The hosted transport intentionally excludes findings, evidence, signing
331
+ digests/keys, apply claims and attempt notes, provider results, and raw run
332
+ errors. Operation before/after values are included because the paired
333
+ organization needs them to review the proposed CRM changes.
334
+
311
335
  The horizontal scheduler: a declarative schedule-entry store, a
312
336
  dependency-free 5-field cron parser, and the read/plan-side `SCHEDULABLE`
313
337
  allowlist. `validateSchedulableArgv` enforces the allowlist at `schedule add`
@@ -94,6 +94,15 @@ replays automatically: reconcile provider state, then `plans recover ...
94
94
  translation, prospect fit scoring, and the agent-driven interview spec.
95
95
  - `acquireMeter.ts` — per-profile windowed budget (records + spend) for acquire.
96
96
  - `acquireSeen.ts` — cross-run "seen" cache so re-runs don't re-pay for dupes.
97
+ - `acquireCheckpoint.ts` — secure profile-scoped, provider/source/list/query-keyed
98
+ continuation store; each audience advances independently.
99
+ - `hostedAcquireCheckpoint.ts` — optional broker-authenticated checkpoint
100
+ transport. Organization identity is resolved server-side; numeric CAS
101
+ revisions prevent stale workers from overwriting newer continuation.
102
+ - `hostedPatchPlan.ts` — best-effort paired-CLI plan mirroring and decision
103
+ reconciliation. Review documents are immutable/hash-bound; hosted approval
104
+ becomes executable only after the CLI verifies it and generates local HMAC
105
+ signatures. Hosted never owns the apply lease for CLI-origin plans.
97
106
  - `assign.ts` — `AssignmentPolicy` (fixed / round-robin / territory /
98
107
  account-owner): the pure owner-routing rule `buildAcquirePlan` stamps onto new
99
108
  leads (never born ownerless) and `reassign --assign-unowned` reuses to backfill.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",