fullstackgtm 0.48.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.
- package/CHANGELOG.md +91 -0
- package/CONTRIBUTING.md +23 -4
- package/DATA-FLOWS.md +8 -2
- package/INSTALL_FOR_AGENTS.md +13 -4
- package/README.md +53 -1
- package/SECURITY.md +27 -3
- package/dist/acquireCheckpoint.d.ts +33 -0
- package/dist/acquireCheckpoint.js +149 -0
- package/dist/acquireLinkedIn.d.ts +2 -0
- package/dist/acquireLinkedIn.js +1 -1
- package/dist/cli/audit.js +10 -7
- package/dist/cli/auth.js +8 -4
- package/dist/cli/enrich.js +283 -79
- package/dist/cli/fix.js +20 -7
- package/dist/cli/help.js +23 -15
- package/dist/cli/market.js +180 -2
- package/dist/cli/plans.js +154 -9
- package/dist/cli/shared.d.ts +3 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli/ui.d.ts +10 -5
- package/dist/cli/ui.js +18 -5
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +23 -2
- package/dist/connectors/linkedin.d.ts +2 -0
- package/dist/connectors/linkedin.js +5 -0
- package/dist/connectors/prospectSources.d.ts +23 -0
- package/dist/connectors/prospectSources.js +25 -14
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/salesforceAuth.d.ts +9 -0
- package/dist/connectors/salesforceAuth.js +47 -5
- package/dist/connectors/theirstack.d.ts +0 -19
- package/dist/connectors/theirstack.js +3 -10
- package/dist/credentials.js +36 -26
- package/dist/enrich.d.ts +44 -1
- package/dist/hostedAcquireCheckpoint.d.ts +43 -0
- package/dist/hostedAcquireCheckpoint.js +129 -0
- package/dist/hostedPatchPlan.d.ts +87 -0
- package/dist/hostedPatchPlan.js +270 -0
- package/dist/icp.js +13 -2
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -2
- package/dist/market.d.ts +1 -1
- package/dist/market.js +7 -127
- package/dist/marketClassify.d.ts +10 -0
- package/dist/marketClassify.js +52 -1
- package/dist/marketSourcing.js +7 -45
- package/dist/mcp.js +67 -115
- package/dist/planStore.d.ts +42 -1
- package/dist/planStore.js +268 -49
- package/dist/progress.d.ts +2 -2
- package/dist/progress.js +2 -2
- package/dist/providerError.d.ts +21 -0
- package/dist/providerError.js +30 -0
- package/dist/publicHttp.d.ts +28 -0
- package/dist/publicHttp.js +143 -0
- package/dist/secureFile.d.ts +15 -0
- package/dist/secureFile.js +164 -0
- package/dist/types.d.ts +1 -1
- package/docs/api.md +53 -2
- package/docs/architecture.md +22 -2
- package/llms.txt +7 -0
- package/package.json +5 -3
- package/skills/fullstackgtm/SKILL.md +8 -2
- package/src/acquireCheckpoint.ts +186 -0
- package/src/acquireLinkedIn.ts +3 -1
- package/src/cli/audit.ts +10 -7
- package/src/cli/auth.ts +8 -4
- package/src/cli/enrich.ts +322 -78
- package/src/cli/fix.ts +28 -7
- package/src/cli/help.ts +24 -15
- package/src/cli/market.ts +181 -2
- package/src/cli/plans.ts +159 -9
- package/src/cli/shared.ts +2 -1
- package/src/cli/ui.ts +19 -9
- package/src/cli.ts +1 -1
- package/src/config.ts +39 -1
- package/src/connectors/linkedin.ts +6 -0
- package/src/connectors/prospectSources.ts +50 -15
- package/src/connectors/salesforce.ts +12 -5
- package/src/connectors/salesforceAuth.ts +46 -6
- package/src/connectors/theirstack.ts +3 -10
- package/src/credentials.ts +47 -28
- package/src/enrich.ts +47 -1
- package/src/hostedAcquireCheckpoint.ts +156 -0
- package/src/hostedPatchPlan.ts +286 -0
- package/src/icp.ts +14 -2
- package/src/index.ts +24 -0
- package/src/market.ts +7 -111
- package/src/marketClassify.ts +61 -1
- package/src/marketSourcing.ts +7 -43
- package/src/mcp.ts +93 -136
- package/src/planStore.ts +322 -57
- package/src/progress.ts +2 -2
- package/src/providerError.ts +37 -0
- package/src/publicHttp.ts +147 -0
- package/src/secureFile.ts +169 -0
- package/src/types.ts +1 -0
package/dist/cli/help.js
CHANGED
|
@@ -176,9 +176,10 @@ Usage:
|
|
|
176
176
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
177
177
|
derive values for requires_human_* placeholders
|
|
178
178
|
from snapshot evidence, with confidence + reasons
|
|
179
|
-
fullstackgtm plans list [--status <s>] | show <id> | reject <id>
|
|
179
|
+
fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>
|
|
180
180
|
fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]
|
|
181
181
|
fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low] [--include-creates]
|
|
182
|
+
fullstackgtm plans recover <id> --acknowledge-uncertain-writes
|
|
182
183
|
fullstackgtm apply --plan-id <id> --provider <name>
|
|
183
184
|
fullstackgtm apply --plan-id <id> --channel outbox (render approved openers; transmits nothing)
|
|
184
185
|
fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [options]
|
|
@@ -199,7 +200,11 @@ Profiles (multi-organization use):
|
|
|
199
200
|
Plan lifecycle:
|
|
200
201
|
audit --save persists the dry-run plan to ~/.fullstackgtm/plans. Approve
|
|
201
202
|
specific operations (optionally with --value <opId>=<v> for placeholders),
|
|
202
|
-
then apply by id — the store enforces approval and records every run.
|
|
203
|
+
then apply by id — the store enforces approval and records every run. When
|
|
204
|
+
paired, local and hosted are replicas of the same immutable plan: either
|
|
205
|
+
capable surface may approve or apply, and each CLI check-in imports missing
|
|
206
|
+
approvals and exact operation receipts. Use \`plans sync\` for an explicit
|
|
207
|
+
check-in; offline changes reconcile later without blocking local work.
|
|
203
208
|
|
|
204
209
|
Authentication (checked in order):
|
|
205
210
|
1. --token-env <name> explicit env var for this invocation (hubspot)
|
|
@@ -226,6 +231,8 @@ Audit options:
|
|
|
226
231
|
--config <path> Config file (default: ./fullstackgtm.config.json if present)
|
|
227
232
|
{ "policy": {...}, "rules": {"enabled":[],"disabled":[]},
|
|
228
233
|
"rulePackages": ["./team-rules.mjs"] }
|
|
234
|
+
--allow-plugins Execute rulePackages from an explicit --config path after review
|
|
235
|
+
--no-plugins Ignore rulePackages; use declarative config and built-in rules only
|
|
229
236
|
--rules <ids> Comma-separated rule ids to run (default: all; see \`rules\`)
|
|
230
237
|
--json Print the JSON patch plan instead of markdown
|
|
231
238
|
--out <path> Also write the JSON patch plan to a file
|
|
@@ -492,14 +499,15 @@ export const HELP = {
|
|
|
492
499
|
seeAlso: ["audit", "plans", "apply"],
|
|
493
500
|
},
|
|
494
501
|
plans: {
|
|
495
|
-
summary: "plan lifecycle: list / show / approve / reject
|
|
502
|
+
summary: "replicated plan lifecycle: list / show / sync / approve / reject",
|
|
496
503
|
phase: "Govern",
|
|
497
504
|
synopsis: [
|
|
498
|
-
"fullstackgtm plans list [--status <s>] | show <id> | reject <id>",
|
|
505
|
+
"fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>",
|
|
499
506
|
"fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
|
|
500
507
|
"fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
|
|
508
|
+
"fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
|
|
501
509
|
],
|
|
502
|
-
detail: "Approval is explicit and per-operation; placeholders need a concrete --value or a suggestion to be approvable.",
|
|
510
|
+
detail: "Approval is explicit and per-operation; placeholders need a concrete --value or a suggestion to be approvable. When paired, hosted and CLI are eventually consistent replicas of one immutable plan. `plans sync` explicitly exchanges approval revisions, execution state, and exact per-operation receipts; ordinary plan commands also check in best-effort. Either surface may apply when its CRM connector supports the selected operations.",
|
|
503
511
|
seeAlso: ["audit", "suggest", "apply"],
|
|
504
512
|
},
|
|
505
513
|
apply: {
|
|
@@ -510,7 +518,7 @@ export const HELP = {
|
|
|
510
518
|
"fullstackgtm apply --plan-id <id> --channel outbox (render approved drafted openers to the outbox; transmits nothing)",
|
|
511
519
|
"fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [--value <opId>=<v>]",
|
|
512
520
|
],
|
|
513
|
-
detail: "The
|
|
521
|
+
detail: "The CLI CRM-write verb. Writes only operations approved locally or imported from the shared hosted replica, with an online execution claim when available, stable operation ids, compare-and-set, resolve-before-create, and readback. Hosted may execute the same synchronized plan when it has a compatible CRM connection; its exact operation receipts are imported on the next CLI check-in. Never writes requires_human_* placeholders without a --value override.",
|
|
514
522
|
seeAlso: ["plans", "suggest", "audit-log"],
|
|
515
523
|
},
|
|
516
524
|
"audit-log": {
|
|
@@ -585,11 +593,11 @@ export const HELP = {
|
|
|
585
593
|
};
|
|
586
594
|
// Verbs that print their own richer multi-subcommand help; runCli routes their
|
|
587
595
|
// `--help` to themselves, so commandHelp() only renders these via `help <verb>`.
|
|
588
|
-
export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "
|
|
596
|
+
export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedule", "signals", "icp", "draft"];
|
|
589
597
|
export const GLOBAL_FLAGS = ["--help", "--full"];
|
|
590
598
|
export const GLOBAL_SHORT_FLAGS = ["-h"];
|
|
591
599
|
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"];
|
|
600
|
+
export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
|
|
593
601
|
// Complete per-command flag registry used by runCli's fail-closed flag
|
|
594
602
|
// validation. Keep this next to HELP so focused help, machine capabilities,
|
|
595
603
|
// and the parser safety gate have one command inventory to reconcile against.
|
|
@@ -605,21 +613,21 @@ export const COMMAND_FLAGS = {
|
|
|
605
613
|
snapshot: [...SOURCE_FLAGS, "--since", "--out", "--archive"],
|
|
606
614
|
audit: [...SOURCE_FLAGS, ...AUDIT_FLAGS],
|
|
607
615
|
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"],
|
|
616
|
+
diff: ["--before", "--after", "--config", "--allow-plugins", "--no-plugins", "--stale-days", "--today", "--json", "--fail-on-new-findings"],
|
|
617
|
+
rules: ["--config", "--allow-plugins", "--no-plugins", "--json"],
|
|
610
618
|
resolve: [...SOURCE_FLAGS, "--name", "--domain", "--email", "--account-id", "--json"],
|
|
611
619
|
hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
|
|
612
620
|
relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
|
|
613
621
|
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"],
|
|
622
|
+
fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
|
|
615
623
|
"bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
616
624
|
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
617
625
|
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
618
626
|
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"],
|
|
627
|
+
enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--json", "--out"],
|
|
620
628
|
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
629
|
suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
|
|
622
|
-
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--json"],
|
|
630
|
+
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
|
|
623
631
|
apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
|
|
624
632
|
"audit-log": ["--in", "--out", "--json"],
|
|
625
633
|
merge: ["--input", "--out", "--json"],
|
|
@@ -628,10 +636,10 @@ export const COMMAND_FLAGS = {
|
|
|
628
636
|
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
|
|
629
637
|
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
|
|
630
638
|
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
|
|
631
|
-
schedule: ["--cron", "--label", "--provider", "--trigger", "--runs", "--json"],
|
|
639
|
+
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--json"],
|
|
632
640
|
};
|
|
633
641
|
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",
|
|
642
|
+
"--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", "--list", "--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", "--scan-limit", "--scopes", "--seed", "--set", "--signals-from", "--since", "--snapshot", "--source", "--staged-run", "--stale-days", "--status", "--task-account", "--task-deal", "--timer", "--title", "--to", "--today", "--token", "--token-env", "--touch", "--transcript", "--trigger", "--usd-per-credit", "--value", "--values-from", "--vendor", "--via", "--watchlist", "--where",
|
|
635
643
|
]);
|
|
636
644
|
// Lifecycle-grouped front door. One line per verb, organized by the
|
|
637
645
|
// Prevent→Detect→Remediate→Verify loop so a new user sees ~6 jobs, not 22 verbs.
|
package/dist/cli/market.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 { 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";
|
|
@@ -14,6 +14,7 @@ import { createFilePlanStore } from "../planStore.js";
|
|
|
14
14
|
import { ENRICH_CONFIG_FILE_NAME, loadEnrichConfig } from "../enrich.js";
|
|
15
15
|
import { loadMeter, recordConsumption, remaining } from "../acquireMeter.js";
|
|
16
16
|
import { progressReporter, reportCounts } from "../runReport.js";
|
|
17
|
+
import { claimHostedPlanApply, reconcileHostedPatchPlan, releaseHostedPlanApply, reportHostedPlanLifecycle, uploadHostedPatchPlan } from "../hostedPatchPlan.js";
|
|
17
18
|
import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
|
|
18
19
|
import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.js";
|
|
19
20
|
import { colorEnabled, createProgressRenderer, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.js";
|
|
@@ -132,9 +133,20 @@ export async function apply(args) {
|
|
|
132
133
|
let valueOverrides;
|
|
133
134
|
const store = planId ? createFilePlanStore() : null;
|
|
134
135
|
if (planId && store) {
|
|
135
|
-
|
|
136
|
+
let stored = await store.get(planId);
|
|
136
137
|
if (!stored)
|
|
137
138
|
throw new Error(`No stored plan with id ${planId}.`);
|
|
139
|
+
const reconciliation = await reconcileHostedPatchPlan(store, stored);
|
|
140
|
+
if (reconciliation.status === "conflict")
|
|
141
|
+
throw new Error(`Refusing to apply plan ${planId}: ${reconciliation.reason}.`);
|
|
142
|
+
if (reconciliation.status === "updated") {
|
|
143
|
+
stored = reconciliation.stored;
|
|
144
|
+
console.error(`Synced hosted ${reconciliation.remoteStatus} state for ${planId}.`);
|
|
145
|
+
}
|
|
146
|
+
if (stored.status === "applied") {
|
|
147
|
+
console.log(`Plan ${planId} was already applied by another replica; imported its execution receipt. No provider writes were attempted.`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
138
150
|
if (stored.status !== "approved") {
|
|
139
151
|
throw new Error(`Plan ${planId} is ${stored.status}; approve operations first with \`fullstackgtm plans approve ${planId} --operations <ids|all>\`.`);
|
|
140
152
|
}
|
|
@@ -210,6 +222,46 @@ export async function apply(args) {
|
|
|
210
222
|
// A channel (e.g. outbox) renders approved ops to a local artifact and
|
|
211
223
|
// transmits nothing; a CRM provider writes records. Same governed apply path.
|
|
212
224
|
const connector = channel ? createChannelConnector(channel) : await connectorFor(provider, args);
|
|
225
|
+
let applyClaimId;
|
|
226
|
+
let hostedApplyClaimId;
|
|
227
|
+
if (planId && store) {
|
|
228
|
+
const claimed = await store.claimApply(planId, { provider: provider ?? `channel:${channel}`, source: "cli" });
|
|
229
|
+
applyClaimId = claimed.claimId;
|
|
230
|
+
const hostedClaim = await claimHostedPlanApply(claimed.stored);
|
|
231
|
+
if (hostedClaim.status === "applied") {
|
|
232
|
+
await store.abortApplyPreflight(planId, claimed.claimId, "Another replica completed apply before provider I/O.");
|
|
233
|
+
const refreshed = await store.get(planId);
|
|
234
|
+
if (refreshed)
|
|
235
|
+
await reconcileHostedPatchPlan(store, refreshed);
|
|
236
|
+
console.log(`Plan ${planId} was already applied by another replica. No provider writes were attempted.`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (hostedClaim.status === "conflict") {
|
|
240
|
+
await store.abortApplyPreflight(planId, claimed.claimId, `Hosted execution claim failed: ${hostedClaim.reason}`);
|
|
241
|
+
throw new Error(`Refusing to apply plan ${planId}: could not coordinate with its hosted replica (${hostedClaim.reason}).`);
|
|
242
|
+
}
|
|
243
|
+
if (hostedClaim.status === "unavailable") {
|
|
244
|
+
console.error(`Hosted replica is unavailable (${hostedClaim.reason}); continuing local-first. Resolve-before-create and provider conflict guards remain active, and the execution receipt will sync on a later check-in.`);
|
|
245
|
+
}
|
|
246
|
+
if (hostedClaim.status === "claimed") {
|
|
247
|
+
hostedApplyClaimId = hostedClaim.claimId;
|
|
248
|
+
await store.recordHostedClaim(planId, claimed.claimId, hostedClaim.claimId);
|
|
249
|
+
}
|
|
250
|
+
const claimedVerification = verifyApprovalDigests(claimed.stored.plan.operations, claimed.stored.approvedOperationIds, claimed.stored.valueOverrides, claimed.stored.approvalDigests);
|
|
251
|
+
if (!claimedVerification.ok) {
|
|
252
|
+
if (hostedApplyClaimId) {
|
|
253
|
+
const released = await releaseHostedPlanApply(claimed.stored, hostedApplyClaimId, "Approval integrity verification failed after claim and before provider I/O.");
|
|
254
|
+
if (released.status === "conflict" || released.status === "unavailable") {
|
|
255
|
+
console.error(`Hosted apply claim may require recovery: ${released.reason}.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
await store.abortApplyPreflight(planId, claimed.claimId, "Approval integrity verification failed after the claim and before provider I/O.");
|
|
259
|
+
throw new Error(`Refusing to apply plan ${planId}: approval changed while acquiring the apply claim.`);
|
|
260
|
+
}
|
|
261
|
+
plan = claimed.stored.plan;
|
|
262
|
+
approvedOperationIds = claimed.stored.approvedOperationIds;
|
|
263
|
+
valueOverrides = claimed.stored.valueOverrides;
|
|
264
|
+
}
|
|
213
265
|
// Interactive terminals get a live apply board on stderr while the run
|
|
214
266
|
// executes (preflight → operations → results, with a per-op safety ticker);
|
|
215
267
|
// piped runs render nothing. Either way the emitter streams heartbeats to
|
|
@@ -224,11 +276,23 @@ export async function apply(args) {
|
|
|
224
276
|
progress,
|
|
225
277
|
});
|
|
226
278
|
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
// Once applyPatchPlan starts, an exception cannot prove that the provider
|
|
281
|
+
// performed no write. Keep the claim fail-closed for operator reconciliation.
|
|
282
|
+
if (planId && store && applyClaimId)
|
|
283
|
+
await store.markApplyUncertain(planId, applyClaimId);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
227
286
|
finally {
|
|
228
287
|
renderer.done();
|
|
229
288
|
}
|
|
230
289
|
if (planId && store) {
|
|
231
|
-
await store.recordRun(planId, run);
|
|
290
|
+
const recorded = await store.recordRun(planId, run, applyClaimId);
|
|
291
|
+
const durableHostedClaimId = recorded.applyAttempts?.find((attempt) => attempt.id === applyClaimId)?.hostedClaimId;
|
|
292
|
+
const mirrored = await reportHostedPlanLifecycle(recorded, { claimId: durableHostedClaimId ?? hostedApplyClaimId });
|
|
293
|
+
if (mirrored.status === "unavailable" || mirrored.status === "conflict") {
|
|
294
|
+
console.error(`Hosted plan status is stale: ${mirrored.reason}. Local apply history remains authoritative.`);
|
|
295
|
+
}
|
|
232
296
|
}
|
|
233
297
|
// Charge the acquire meter for the creates that actually landed.
|
|
234
298
|
if (createOps.length > 0) {
|
|
@@ -272,8 +336,9 @@ export async function diffCommand(args) {
|
|
|
272
336
|
}
|
|
273
337
|
const before = JSON.parse(readFileSync(resolve(process.cwd(), beforePath), "utf8"));
|
|
274
338
|
const after = JSON.parse(readFileSync(resolve(process.cwd(), afterPath), "utf8"));
|
|
275
|
-
const
|
|
276
|
-
const
|
|
339
|
+
const explicitConfig = option(args, "--config") ?? undefined;
|
|
340
|
+
const loaded = loadConfig(explicitConfig);
|
|
341
|
+
const rules = selectedRules(args, await resolveConfiguredRules(loaded, undefined, rulePackageTrustFromCli(args, explicitConfig)));
|
|
277
342
|
const policy = mergePolicy(defaultPolicy(), loaded?.config);
|
|
278
343
|
const today = option(args, "--today");
|
|
279
344
|
if (today)
|
|
@@ -332,7 +397,14 @@ export async function plansCommand(args) {
|
|
|
332
397
|
const [subcommand, ...rest] = args;
|
|
333
398
|
if (subcommand === "list" || subcommand === undefined) {
|
|
334
399
|
const status = option(rest, "--status");
|
|
335
|
-
|
|
400
|
+
let plans = await store.list(status ?? undefined);
|
|
401
|
+
const synced = await Promise.all(plans.map(async (stored) => {
|
|
402
|
+
const result = await reconcileHostedPatchPlan(store, stored);
|
|
403
|
+
if (result.status === "updated")
|
|
404
|
+
return result.stored;
|
|
405
|
+
return stored;
|
|
406
|
+
}));
|
|
407
|
+
plans = status ? synced.filter((stored) => stored.status === status) : synced;
|
|
336
408
|
if (rest.includes("--json") || args.includes("--json")) {
|
|
337
409
|
console.log(JSON.stringify(plans.map((stored) => ({
|
|
338
410
|
id: stored.plan.id,
|
|
@@ -386,9 +458,17 @@ export async function plansCommand(args) {
|
|
|
386
458
|
const planId = rest.find((arg) => !arg.startsWith("--"));
|
|
387
459
|
if (!planId)
|
|
388
460
|
throw new Error("Usage: fullstackgtm plans show <planId>");
|
|
389
|
-
|
|
461
|
+
let stored = await store.get(planId);
|
|
390
462
|
if (!stored)
|
|
391
463
|
throw new Error(`No stored plan with id ${planId}.`);
|
|
464
|
+
const reconciliation = await reconcileHostedPatchPlan(store, stored);
|
|
465
|
+
if (reconciliation.status === "updated") {
|
|
466
|
+
stored = reconciliation.stored;
|
|
467
|
+
console.error(`Synced hosted ${reconciliation.remoteStatus} state for ${planId}.`);
|
|
468
|
+
}
|
|
469
|
+
else if (reconciliation.status === "conflict") {
|
|
470
|
+
console.error(`Hosted sync conflict: ${reconciliation.reason}.`);
|
|
471
|
+
}
|
|
392
472
|
if (rest.includes("--json")) {
|
|
393
473
|
console.log(JSON.stringify(stored, null, 2));
|
|
394
474
|
return;
|
|
@@ -397,6 +477,19 @@ export async function plansCommand(args) {
|
|
|
397
477
|
console.log(`Status: ${planStatusWord(stored.status, showPaint)}`);
|
|
398
478
|
console.log(`Approved operations: ${stored.approvedOperationIds.join(", ") || "none"}`);
|
|
399
479
|
console.log(`Runs: ${stored.runs.length}`);
|
|
480
|
+
const mirror = await uploadHostedPatchPlan(stored);
|
|
481
|
+
if (mirror.status === "saved")
|
|
482
|
+
console.log(`Hosted review: ${mirror.state.url}`);
|
|
483
|
+
else if (mirror.status === "unavailable" || mirror.status === "conflict")
|
|
484
|
+
console.log(`Hosted review: sync pending (${mirror.reason})`);
|
|
485
|
+
if (stored.applyAttempts?.length) {
|
|
486
|
+
console.log("Apply attempts:");
|
|
487
|
+
for (const attempt of stored.applyAttempts) {
|
|
488
|
+
console.log(` ${attempt.id} ${attempt.status} ${attempt.provider} via ${attempt.source} ${attempt.claimedAt}`);
|
|
489
|
+
if (attempt.note)
|
|
490
|
+
console.log(` ${attempt.note}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
400
493
|
console.log("");
|
|
401
494
|
console.log(stylizePlanMarkdown(patchPlanToMarkdown(stored.plan), showPaint));
|
|
402
495
|
return;
|
|
@@ -441,6 +534,10 @@ export async function plansCommand(args) {
|
|
|
441
534
|
...fileOverrides,
|
|
442
535
|
...explicitOverrides,
|
|
443
536
|
});
|
|
537
|
+
const mirror = await reportHostedPlanLifecycle(updated);
|
|
538
|
+
if (mirror.status === "unavailable" || mirror.status === "conflict") {
|
|
539
|
+
console.error(`Hosted plan status is stale: ${mirror.reason}. Local approval remains authoritative.`);
|
|
540
|
+
}
|
|
444
541
|
console.log(`Approved ${updated.approvedOperationIds.length} operation(s) on ${planId}. Apply with \`fullstackgtm apply --plan-id ${planId} --provider <name>\`.`);
|
|
445
542
|
return;
|
|
446
543
|
}
|
|
@@ -448,9 +545,57 @@ export async function plansCommand(args) {
|
|
|
448
545
|
const planId = rest.find((arg) => !arg.startsWith("--"));
|
|
449
546
|
if (!planId)
|
|
450
547
|
throw new Error("Usage: fullstackgtm plans reject <planId>");
|
|
451
|
-
await store.reject(planId);
|
|
548
|
+
const rejected = await store.reject(planId);
|
|
549
|
+
const mirror = await reportHostedPlanLifecycle(rejected);
|
|
550
|
+
if (mirror.status === "unavailable" || mirror.status === "conflict") {
|
|
551
|
+
console.error(`Hosted plan status is stale: ${mirror.reason}. Local rejection remains authoritative.`);
|
|
552
|
+
}
|
|
452
553
|
console.log(`Rejected ${planId}.`);
|
|
453
554
|
return;
|
|
454
555
|
}
|
|
455
|
-
|
|
556
|
+
if (subcommand === "recover") {
|
|
557
|
+
const planId = rest.find((arg) => !arg.startsWith("--"));
|
|
558
|
+
if (!planId) {
|
|
559
|
+
throw new Error("Usage: fullstackgtm plans recover <planId> --acknowledge-uncertain-writes");
|
|
560
|
+
}
|
|
561
|
+
const stored = await store.get(planId);
|
|
562
|
+
if (!stored)
|
|
563
|
+
throw new Error(`No stored plan with id ${planId}.`);
|
|
564
|
+
if (stored.status !== "applying" || !stored.applyClaim) {
|
|
565
|
+
throw new Error(`Plan ${planId} has no unresolved apply attempt.`);
|
|
566
|
+
}
|
|
567
|
+
const attempt = stored.applyAttempts?.find((entry) => entry.id === stored.applyClaim?.id);
|
|
568
|
+
if (!rest.includes("--acknowledge-uncertain-writes")) {
|
|
569
|
+
throw new Error(`Plan ${planId} has an unresolved ${attempt?.provider ?? "provider"} apply attempt (${stored.applyClaim.id}). ` +
|
|
570
|
+
"Inspect the affected records in the provider; do not replay automatically. If you accept that some writes may already have landed, " +
|
|
571
|
+
`run \`fullstackgtm plans recover ${planId} --acknowledge-uncertain-writes\`. This clears every approval and requires a fresh review and approval before retrying.`);
|
|
572
|
+
}
|
|
573
|
+
await store.recoverApply(planId);
|
|
574
|
+
console.log(`Released ${planId} to needs_approval after acknowledging uncertain provider state. ` +
|
|
575
|
+
`No writes were replayed. Re-audit provider state, review the plan, then approve operations again before any apply.`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (subcommand === "sync") {
|
|
579
|
+
const plans = await store.list();
|
|
580
|
+
let updated = 0;
|
|
581
|
+
const warnings = [];
|
|
582
|
+
for (const stored of plans) {
|
|
583
|
+
const result = await reconcileHostedPatchPlan(store, stored);
|
|
584
|
+
if (result.status === "updated")
|
|
585
|
+
updated += 1;
|
|
586
|
+
else if (result.status === "conflict" || result.status === "unavailable")
|
|
587
|
+
warnings.push(`${stored.plan.id}: ${result.reason}`);
|
|
588
|
+
if (stored.status === "applied") {
|
|
589
|
+
const hostedClaimId = [...(stored.applyAttempts ?? [])].reverse().find((attempt) => attempt.hostedClaimId)?.hostedClaimId;
|
|
590
|
+
const pushed = await reportHostedPlanLifecycle(stored, { claimId: hostedClaimId });
|
|
591
|
+
if (pushed.status === "conflict" || pushed.status === "unavailable")
|
|
592
|
+
warnings.push(`${stored.plan.id} receipt: ${pushed.reason}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
console.log(`Plan replicas synchronized: ${updated} updated, ${plans.length - updated} unchanged.`);
|
|
596
|
+
for (const warning of warnings)
|
|
597
|
+
console.error(` ${warning}`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
throw unknownSubcommandError("plans", subcommand, ["list", "show", "sync", "approve", "reject", "recover"]);
|
|
456
601
|
}
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -23,7 +23,9 @@ export declare function saveRequested(args: string[]): boolean;
|
|
|
23
23
|
export declare function confirmRequested(args: string[], ...legacyAliases: string[]): boolean;
|
|
24
24
|
export declare function numericOption(args: string[], name: string): number | undefined;
|
|
25
25
|
export declare function connectorFor(provider: string, args: string[], progress?: ProgressEmitter): Promise<GtmConnector>;
|
|
26
|
-
export declare function readSnapshot(args: string[], progress?: ProgressEmitter
|
|
26
|
+
export declare function readSnapshot(args: string[], progress?: ProgressEmitter, options?: {
|
|
27
|
+
persistProgress?: boolean;
|
|
28
|
+
}): Promise<CanonicalGtmSnapshot>;
|
|
27
29
|
/**
|
|
28
30
|
* Validate that an --input file actually has the canonical snapshot shape
|
|
29
31
|
* (the JSON `snapshot --out` writes) instead of blindly casting — a plan or
|
package/dist/cli/shared.js
CHANGED
|
@@ -131,7 +131,7 @@ export async function connectorFor(provider, args, progress) {
|
|
|
131
131
|
throw new Error(`Unknown provider: ${provider}.${providerSuggestion ? ` Did you mean ${providerSuggestion}?` : ""} ` +
|
|
132
132
|
"Supported providers: hubspot, salesforce, stripe");
|
|
133
133
|
}
|
|
134
|
-
export async function readSnapshot(args, progress) {
|
|
134
|
+
export async function readSnapshot(args, progress, options = {}) {
|
|
135
135
|
const provider = option(args, "--provider");
|
|
136
136
|
if (provider) {
|
|
137
137
|
// A verb driving its own progress board (e.g. `backfill stripe`) passes
|
|
@@ -153,7 +153,7 @@ export async function readSnapshot(args, progress) {
|
|
|
153
153
|
return await connector.fetchSnapshot();
|
|
154
154
|
}
|
|
155
155
|
finally {
|
|
156
|
-
renderer.done();
|
|
156
|
+
renderer.done({ persist: options.persistProgress });
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
if (args.includes("--demo")) {
|