fullstackgtm 0.49.0 → 0.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/DATA-FLOWS.md +1 -0
- package/README.md +48 -0
- 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 +6 -1
- package/dist/cli/auth.js +25 -0
- package/dist/cli/backfill.js +4 -0
- package/dist/cli/call.js +28 -6
- package/dist/cli/draft.js +11 -1
- package/dist/cli/enrich.js +286 -81
- package/dist/cli/fix.js +6 -3
- package/dist/cli/help.js +30 -26
- package/dist/cli/icp.js +15 -1
- package/dist/cli/market.js +14 -1
- package/dist/cli/planOutput.d.ts +5 -0
- package/dist/cli/planOutput.js +27 -0
- package/dist/cli/plans.js +269 -24
- package/dist/cli/schedule.js +22 -11
- package/dist/cli/shared.d.ts +3 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli/signals.js +22 -3
- package/dist/cli/tam.js +10 -1
- package/dist/cli/ui.d.ts +10 -5
- package/dist/cli/ui.js +28 -9
- package/dist/connectors/hubspot.d.ts +4 -0
- package/dist/connectors/hubspot.js +36 -18
- 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 +21 -3
- 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 +275 -0
- package/dist/icp.js +13 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/planStore.d.ts +14 -0
- package/dist/planStore.js +73 -0
- package/dist/progress.d.ts +2 -2
- package/dist/progress.js +2 -2
- package/docs/api.md +24 -0
- package/docs/architecture.md +9 -0
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +1 -1
- package/src/acquireCheckpoint.ts +186 -0
- package/src/acquireLinkedIn.ts +3 -1
- package/src/cli/audit.ts +5 -1
- package/src/cli/auth.ts +24 -0
- package/src/cli/backfill.ts +3 -0
- package/src/cli/call.ts +25 -6
- package/src/cli/draft.ts +9 -1
- package/src/cli/enrich.ts +325 -80
- package/src/cli/fix.ts +5 -3
- package/src/cli/help.ts +31 -26
- package/src/cli/icp.ts +13 -1
- package/src/cli/market.ts +12 -1
- package/src/cli/planOutput.ts +30 -0
- package/src/cli/plans.ts +259 -25
- package/src/cli/schedule.ts +21 -15
- package/src/cli/shared.ts +2 -1
- package/src/cli/signals.ts +22 -3
- package/src/cli/tam.ts +8 -1
- package/src/cli/ui.ts +31 -13
- package/src/connectors/hubspot.ts +41 -17
- package/src/connectors/linkedin.ts +6 -0
- package/src/connectors/prospectSources.ts +46 -4
- package/src/enrich.ts +47 -1
- package/src/hostedAcquireCheckpoint.ts +156 -0
- package/src/hostedPatchPlan.ts +291 -0
- package/src/icp.ts +14 -2
- package/src/index.ts +20 -0
- package/src/planStore.ts +87 -0
- package/src/progress.ts +2 -2
package/src/cli/help.ts
CHANGED
|
@@ -180,11 +180,11 @@ Usage:
|
|
|
180
180
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
181
181
|
derive values for requires_human_* placeholders
|
|
182
182
|
from snapshot evidence, with confidence + reasons
|
|
183
|
-
fullstackgtm plans list [--status <s>] | show <id> | reject <id>
|
|
183
|
+
fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>
|
|
184
184
|
fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]
|
|
185
185
|
fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low] [--include-creates]
|
|
186
186
|
fullstackgtm plans recover <id> --acknowledge-uncertain-writes
|
|
187
|
-
fullstackgtm apply --plan-id <id> --provider <name>
|
|
187
|
+
fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]
|
|
188
188
|
fullstackgtm apply --plan-id <id> --channel outbox (render approved openers; transmits nothing)
|
|
189
189
|
fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [options]
|
|
190
190
|
fullstackgtm audit-log export [--out <path>] | verify --in <path> tamper-evident apply-run record
|
|
@@ -204,7 +204,11 @@ Profiles (multi-organization use):
|
|
|
204
204
|
Plan lifecycle:
|
|
205
205
|
audit --save persists the dry-run plan to ~/.fullstackgtm/plans. Approve
|
|
206
206
|
specific operations (optionally with --value <opId>=<v> for placeholders),
|
|
207
|
-
then apply by id — the store enforces approval and records every run.
|
|
207
|
+
then apply by id — the store enforces approval and records every run. When
|
|
208
|
+
paired, local and hosted are replicas of the same immutable plan: either
|
|
209
|
+
capable surface may approve or apply, and each CLI check-in imports missing
|
|
210
|
+
approvals and exact operation receipts. Use \`plans sync\` for an explicit
|
|
211
|
+
check-in; offline changes reconcile later without blocking local work.
|
|
208
212
|
|
|
209
213
|
Authentication (checked in order):
|
|
210
214
|
1. --token-env <name> explicit env var for this invocation (hubspot)
|
|
@@ -543,27 +547,28 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
543
547
|
seeAlso: ["audit", "plans", "apply"],
|
|
544
548
|
},
|
|
545
549
|
plans: {
|
|
546
|
-
summary: "plan lifecycle: list / show / approve / reject
|
|
550
|
+
summary: "replicated plan lifecycle: list / show / sync / approve / reject",
|
|
547
551
|
phase: "Govern",
|
|
548
552
|
synopsis: [
|
|
549
|
-
"fullstackgtm plans list [--status <s>] | show <id> | reject <id>",
|
|
553
|
+
"fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>",
|
|
550
554
|
"fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
|
|
551
555
|
"fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
|
|
552
556
|
"fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
|
|
553
557
|
],
|
|
554
|
-
detail:
|
|
558
|
+
detail:
|
|
559
|
+
"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.",
|
|
555
560
|
seeAlso: ["audit", "suggest", "apply"],
|
|
556
561
|
},
|
|
557
562
|
apply: {
|
|
558
563
|
summary: "write ONLY explicitly approved operations to a provider",
|
|
559
564
|
phase: "Govern / Verify",
|
|
560
565
|
synopsis: [
|
|
561
|
-
"fullstackgtm apply --plan-id <id> --provider <name>",
|
|
566
|
+
"fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]",
|
|
562
567
|
"fullstackgtm apply --plan-id <id> --channel outbox (render approved drafted openers to the outbox; transmits nothing)",
|
|
563
568
|
"fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [--value <opId>=<v>]",
|
|
564
569
|
],
|
|
565
570
|
detail:
|
|
566
|
-
"The
|
|
571
|
+
"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. Default output is a compact outcome card; --verbose prints the full run document and --json preserves structured output. 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.",
|
|
567
572
|
seeAlso: ["plans", "suggest", "audit-log"],
|
|
568
573
|
},
|
|
569
574
|
"audit-log": {
|
|
@@ -653,7 +658,7 @@ export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedul
|
|
|
653
658
|
export const GLOBAL_FLAGS = ["--help", "--full"];
|
|
654
659
|
export const GLOBAL_SHORT_FLAGS = ["-h"];
|
|
655
660
|
export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
|
|
656
|
-
export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
|
|
661
|
+
export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full", "--verbose"];
|
|
657
662
|
|
|
658
663
|
// Complete per-command flag registry used by runCli's fail-closed flag
|
|
659
664
|
// validation. Keep this next to HELP so focused help, machine capabilities,
|
|
@@ -662,7 +667,7 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
662
667
|
init: ["--source", "--provider", "--out", "--force"],
|
|
663
668
|
login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
|
|
664
669
|
logout: [],
|
|
665
|
-
doctor: ["--json"],
|
|
670
|
+
doctor: ["--verbose", "--json"],
|
|
666
671
|
capabilities: ["--json"],
|
|
667
672
|
"robot-docs": [],
|
|
668
673
|
profiles: ["--json"],
|
|
@@ -676,28 +681,28 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
676
681
|
hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
|
|
677
682
|
relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
|
|
678
683
|
route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
679
|
-
fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
|
|
680
|
-
"bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
681
|
-
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
682
|
-
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
683
|
-
backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--json"],
|
|
684
|
-
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"],
|
|
685
|
-
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"],
|
|
684
|
+
fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run", "--verbose"],
|
|
685
|
+
"bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
686
|
+
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
687
|
+
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
688
|
+
backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--verbose", "--json"],
|
|
689
|
+
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", "--verbose", "--json", "--out"],
|
|
690
|
+
call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--verbose", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
|
|
686
691
|
suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
|
|
687
|
-
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
|
|
688
|
-
apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
|
|
692
|
+
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--verbose", "--json"],
|
|
693
|
+
apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--verbose", "--json", "--config"],
|
|
689
694
|
"audit-log": ["--in", "--out", "--json"],
|
|
690
695
|
merge: ["--input", "--out", "--json"],
|
|
691
|
-
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"],
|
|
692
|
-
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"],
|
|
693
|
-
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
|
|
694
|
-
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
|
|
695
|
-
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
|
|
696
|
-
schedule: ["--cron", "--label", "--provider", "--trigger", "--runs", "--json"],
|
|
696
|
+
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", "--verbose", "--json", "--out"],
|
|
697
|
+
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", "--verbose", "--json", "--out"],
|
|
698
|
+
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
|
|
699
|
+
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
700
|
+
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
701
|
+
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
|
|
697
702
|
};
|
|
698
703
|
|
|
699
704
|
export const FLAGS_WITH_VALUES = new Set([
|
|
700
|
-
"--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",
|
|
705
|
+
"--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",
|
|
701
706
|
]);
|
|
702
707
|
|
|
703
708
|
// Lifecycle-grouped front door. One line per verb, organized by the
|
package/src/cli/icp.ts
CHANGED
|
@@ -12,6 +12,18 @@ import { loadIcp, numericOption, option, readSnapshot, resolveLlmBaseUrls, saveR
|
|
|
12
12
|
import { createStatusLine } from "./ui.ts";
|
|
13
13
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
14
14
|
|
|
15
|
+
function renderJudgeDecisions(decisions: Awaited<ReturnType<typeof judgeSignals>>): string {
|
|
16
|
+
if (decisions.length === 0) return "No accounts cleared the score threshold.";
|
|
17
|
+
const lines = [`ICP decisions (${decisions.length})`, ""];
|
|
18
|
+
for (const decision of decisions) {
|
|
19
|
+
const target = decision.contact?.email || decision.contact?.title;
|
|
20
|
+
lines.push(`${decision.decision.toUpperCase().padEnd(7)} ${String(decision.score).padStart(3)} ${decision.accountDomain}${target ? ` · ${target}` : ""}`);
|
|
21
|
+
if (decision.whyNow) lines.push(` ${decision.whyNow.replace(/\s+/g, " ").trim()}`);
|
|
22
|
+
if (decision.play) lines.push(` Next: ${decision.play.replace(/\s+/g, " ").trim()}`);
|
|
23
|
+
}
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
29
|
* `icp` — develop and inspect the Ideal Customer Profile that targets acquire.
|
|
@@ -158,7 +170,7 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
158
170
|
decisions = decisions.filter((d) => d.score >= minScore);
|
|
159
171
|
|
|
160
172
|
// 5) Ranked decisions to stdout (JSON), guidance to stderr.
|
|
161
|
-
console.log(JSON.stringify(decisions, null, 2));
|
|
173
|
+
console.log(rest.includes("--json") || rest.includes("--verbose") ? JSON.stringify(decisions, null, 2) : renderJudgeDecisions(decisions));
|
|
162
174
|
console.error(
|
|
163
175
|
`Judged ${unjudged.length} signal(s) across ${new Set(decisions.map((d) => d.accountDomain)).size} account(s): ` +
|
|
164
176
|
`${decisions.filter((d) => d.decision === "send").length} send, ` +
|
package/src/cli/market.ts
CHANGED
|
@@ -499,8 +499,19 @@ Rules:
|
|
|
499
499
|
if (outPath) {
|
|
500
500
|
writeFileSync(resolve(process.cwd(), outPath), output);
|
|
501
501
|
console.log(`Wrote ${outPath}`);
|
|
502
|
-
} else {
|
|
502
|
+
} else if (rest.includes("--verbose") || option(rest, "--format")) {
|
|
503
503
|
console.log(output);
|
|
504
|
+
} else {
|
|
505
|
+
const fronts = computeFrontStates(config, set);
|
|
506
|
+
const counts = new Map<string, number>();
|
|
507
|
+
for (const front of fronts) counts.set(front.state, (counts.get(front.state) ?? 0) + 1);
|
|
508
|
+
console.log(`${config.category} market · ${set.runLabel}`);
|
|
509
|
+
console.log(`${fronts.length} claim fronts · ${["open", "contested", "owned", "saturated", "vacant"].filter((state) => counts.has(state)).map((state) => `${counts.get(state)} ${state}`).join(" · ")}`);
|
|
510
|
+
for (const front of fronts) {
|
|
511
|
+
const owner = front.loudVendorIds.length ? ` · ${front.loudVendorIds.join(", ")}` : "";
|
|
512
|
+
console.log(`\n${front.state.toUpperCase()} ${front.claimId}${owner}`);
|
|
513
|
+
}
|
|
514
|
+
console.log("\nUse --verbose for the full evidence report, or --out <path> to write it.");
|
|
504
515
|
}
|
|
505
516
|
return;
|
|
506
517
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PatchPlan } from "../types.ts";
|
|
2
|
+
import { box, colorEnabled, paint, truncateToWidth } from "./ui.ts";
|
|
3
|
+
|
|
4
|
+
export function compactPlan(plan: PatchPlan, options: { saved?: boolean } = {}): string {
|
|
5
|
+
const counts = new Map<string, number>();
|
|
6
|
+
for (const operation of plan.operations) {
|
|
7
|
+
const action = operation.operation || operation.field || "change";
|
|
8
|
+
counts.set(action, (counts.get(action) ?? 0) + 1);
|
|
9
|
+
}
|
|
10
|
+
const mix = [...counts]
|
|
11
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
12
|
+
.slice(0, 4)
|
|
13
|
+
.map(([action, count]) => `${count} ${action}`)
|
|
14
|
+
.join(" · ");
|
|
15
|
+
const lines = [
|
|
16
|
+
truncateToWidth(plan.title, 100),
|
|
17
|
+
`Status ${options.saved ? "SAVED · NEEDS APPROVAL" : "DRY RUN · NOTHING WRITTEN"}`,
|
|
18
|
+
truncateToWidth(`Scope ${plan.operations.length} operation${plan.operations.length === 1 ? "" : "s"}${mix ? ` · ${mix}` : ""}`, 100),
|
|
19
|
+
truncateToWidth(`Effect ${plan.summary}`, 100),
|
|
20
|
+
options.saved ? `Next fullstackgtm plans show ${plan.id}` : "Next re-run with --save to stage this plan for approval",
|
|
21
|
+
];
|
|
22
|
+
return [
|
|
23
|
+
...box(lines, paint(colorEnabled(process.stdout)), "Plan preview"),
|
|
24
|
+
`Details: re-run with --verbose${options.saved ? `; JSON: fullstackgtm plans show ${plan.id} --json` : "; machine output: --json"}`,
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function verbosePlanRequested(args: readonly string[]) {
|
|
29
|
+
return args.includes("--verbose") || args.includes("--full");
|
|
30
|
+
}
|
package/src/cli/plans.ts
CHANGED
|
@@ -11,15 +11,16 @@ import { formatPatchPlanRun, patchPlanToMarkdown } from "../format.ts";
|
|
|
11
11
|
import { mergeSnapshots } from "../merge.ts";
|
|
12
12
|
import { verifyApprovalDigests } from "../integrity.ts";
|
|
13
13
|
import { buildAuditLog, verifyAuditLog } from "../auditLog.ts";
|
|
14
|
-
import { createFilePlanStore } from "../planStore.ts";
|
|
14
|
+
import { createFilePlanStore, type StoredPlan } from "../planStore.ts";
|
|
15
15
|
import { ENRICH_CONFIG_FILE_NAME, loadEnrichConfig, type EnrichConfig } from "../enrich.ts";
|
|
16
16
|
import { loadMeter, recordConsumption, remaining } from "../acquireMeter.ts";
|
|
17
17
|
import { progressReporter, reportCounts } from "../runReport.ts";
|
|
18
|
+
import { claimHostedPlanApply, reconcileHostedPatchPlan, releaseHostedPlanApply, reportHostedPlanLifecycle, uploadHostedPatchPlan } from "../hostedPatchPlan.ts";
|
|
18
19
|
import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progress.ts";
|
|
19
20
|
import type { ValueSuggestion } from "../suggest.ts";
|
|
20
21
|
import type { CanonicalGtmSnapshot, CreateRecordPayload, PatchOperation, PatchPlan, PatchPlanRun } from "../types.ts";
|
|
21
22
|
import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.ts";
|
|
22
|
-
import { colorEnabled, createProgressRenderer, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.ts";
|
|
23
|
+
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatDuration, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.ts";
|
|
23
24
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
24
25
|
|
|
25
26
|
|
|
@@ -33,6 +34,115 @@ function parseValueOverrides(args: string[]) {
|
|
|
33
34
|
return valueOverrides;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function recordHeadline(operation: PatchOperation): { title: string; subtitle?: string; company?: string } {
|
|
38
|
+
const after = operation.afterValue && typeof operation.afterValue === "object" && !Array.isArray(operation.afterValue)
|
|
39
|
+
? operation.afterValue as Record<string, unknown> : {};
|
|
40
|
+
const properties = after.properties && typeof after.properties === "object" && !Array.isArray(after.properties)
|
|
41
|
+
? after.properties as Record<string, unknown> : {};
|
|
42
|
+
const first = typeof properties.firstname === "string" ? properties.firstname : "";
|
|
43
|
+
const last = typeof properties.lastname === "string" ? properties.lastname : "";
|
|
44
|
+
const company = typeof properties.company === "string" ? properties.company
|
|
45
|
+
: typeof after.associateCompanyName === "string" ? after.associateCompanyName : undefined;
|
|
46
|
+
const title = [first, last].filter(Boolean).join(" ") || company || operation.objectId || operation.id;
|
|
47
|
+
const role = typeof properties.jobtitle === "string" ? properties.jobtitle : undefined;
|
|
48
|
+
return { title, ...(role ? { subtitle: role } : {}), ...(company ? { company } : {}) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function planEffect(stored: StoredPlan): string {
|
|
52
|
+
const total = stored.plan.operations.length;
|
|
53
|
+
const approved = stored.approvedOperationIds.length;
|
|
54
|
+
if (stored.status === "applied") {
|
|
55
|
+
const results = stored.runs.at(-1)?.results ?? [];
|
|
56
|
+
const applied = results.filter((result) => result.status === "applied").length;
|
|
57
|
+
const skipped = results.filter((result) => result.status === "skipped").length;
|
|
58
|
+
return `Completed ${applied} operation${applied === 1 ? "" : "s"}${skipped ? `; ${skipped} skipped` : ""}. No further writes will run.`;
|
|
59
|
+
}
|
|
60
|
+
if (stored.status === "approved") return `Apply will execute ${approved} selected operation${approved === 1 ? "" : "s"}; ${total - approved} will not run.`;
|
|
61
|
+
if (stored.status === "rejected") return "Rejected. No provider writes can run.";
|
|
62
|
+
return `No provider writes can run until operations are approved (0 of ${total} selected).`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printPlanCards(stored: StoredPlan, hostedUrl?: string) {
|
|
66
|
+
const p = paint(colorEnabled(process.stdout));
|
|
67
|
+
const total = stored.plan.operations.length;
|
|
68
|
+
const approved = new Set(stored.approvedOperationIds);
|
|
69
|
+
const width = Math.max(56, Math.min(100, (process.stdout.columns ?? 100) - 4));
|
|
70
|
+
const fit = (value: string) => truncateToWidth(value, width);
|
|
71
|
+
const summary = [
|
|
72
|
+
fit(stored.plan.title),
|
|
73
|
+
`Status ${stored.status.toUpperCase().replaceAll("_", " ")}`,
|
|
74
|
+
`Selection ${approved.size} of ${total} operation${total === 1 ? "" : "s"} approved`,
|
|
75
|
+
`Runs ${stored.runs.length}`,
|
|
76
|
+
fit(`Effect ${planEffect(stored)}`),
|
|
77
|
+
];
|
|
78
|
+
console.log(box(summary, p, "Plan").join("\n"));
|
|
79
|
+
if (hostedUrl) console.log(`Hosted: ${hostedUrl}`);
|
|
80
|
+
console.log("\nOperations");
|
|
81
|
+
let operationIndex = 0;
|
|
82
|
+
const latestResults = new Map((stored.runs.at(-1)?.results ?? []).map((result) => [result.operationId, result.status]));
|
|
83
|
+
for (const operation of stored.plan.operations) {
|
|
84
|
+
const selected = approved.has(operation.id);
|
|
85
|
+
const resultStatus = latestResults.get(operation.id);
|
|
86
|
+
const headline = recordHeadline(operation);
|
|
87
|
+
const action = operation.operation === "create_record"
|
|
88
|
+
? `Create ${operation.objectType}${headline.company ? ` and resolve/link ${headline.company}` : ""}`
|
|
89
|
+
: `${operation.operation.replaceAll("_", " ")} ${operation.objectType}`;
|
|
90
|
+
const lines = [
|
|
91
|
+
fit(headline.title),
|
|
92
|
+
...(headline.subtitle || headline.company
|
|
93
|
+
? [fit([headline.subtitle, headline.company].filter(Boolean).join(" · "))]
|
|
94
|
+
: []),
|
|
95
|
+
fit(`Action ${action}`),
|
|
96
|
+
`Operation ${operation.id}`,
|
|
97
|
+
];
|
|
98
|
+
if (operationIndex++ > 0) console.log("");
|
|
99
|
+
const state = resultStatus === "applied" ? "✓ APPLIED"
|
|
100
|
+
: resultStatus && selected ? `! ${resultStatus.toUpperCase()}`
|
|
101
|
+
: selected ? "✓ APPROVED" : stored.status === "applied" ? "○ EXCLUDED" : "○ NOT APPROVED";
|
|
102
|
+
console.log(box(lines, p, state).join("\n"));
|
|
103
|
+
}
|
|
104
|
+
if (stored.status === "approved") {
|
|
105
|
+
console.log(`\nNext: fullstackgtm apply --plan-id ${stored.plan.id} --provider <hubspot|salesforce>`);
|
|
106
|
+
}
|
|
107
|
+
console.log(`Details: fullstackgtm plans show ${stored.plan.id} --verbose`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printApplyCards(run: PatchPlanRun, plan: PatchPlan, approvedOperationIds: string[], planIdStored: boolean) {
|
|
111
|
+
const p = paint(colorEnabled(process.stdout));
|
|
112
|
+
const approved = new Set(approvedOperationIds);
|
|
113
|
+
const operations = new Map(plan.operations.map((operation) => [operation.id, operation]));
|
|
114
|
+
const counts = { applied: 0, skipped: 0, failed: 0, conflict: 0 };
|
|
115
|
+
for (const result of run.results) counts[result.status] += 1;
|
|
116
|
+
const elapsed = Math.max(0, Date.parse(run.finishedAt) - Date.parse(run.startedAt));
|
|
117
|
+
const summary = [
|
|
118
|
+
`Status ${run.status.toUpperCase()}`,
|
|
119
|
+
`Provider ${run.provider}`,
|
|
120
|
+
`Outcome ${counts.applied} applied · ${run.results.length - approved.size} excluded · ${counts.failed + counts.conflict} failed/conflicted`,
|
|
121
|
+
`Duration ${formatDuration(elapsed)}`,
|
|
122
|
+
...(planIdStored ? ["Receipt recorded locally; hosted reconciliation requested"] : []),
|
|
123
|
+
];
|
|
124
|
+
console.log(box(summary, p, "Apply result").join("\n"));
|
|
125
|
+
console.log("\nOperations");
|
|
126
|
+
let index = 0;
|
|
127
|
+
for (const result of run.results) {
|
|
128
|
+
const operation = operations.get(result.operationId);
|
|
129
|
+
if (!operation) continue;
|
|
130
|
+
const selected = approved.has(result.operationId);
|
|
131
|
+
const headline = recordHeadline(operation);
|
|
132
|
+
const state = !selected ? "○ EXCLUDED" : result.status === "applied" ? "✓ APPLIED" : `! ${result.status.toUpperCase()}`;
|
|
133
|
+
const lines = [
|
|
134
|
+
headline.title,
|
|
135
|
+
...(headline.subtitle || headline.company ? [[headline.subtitle, headline.company].filter(Boolean).join(" · ")] : []),
|
|
136
|
+
`Operation ${result.operationId}`,
|
|
137
|
+
...(result.detail ? [truncateToWidth(`Result ${result.detail}`, 100)] : []),
|
|
138
|
+
];
|
|
139
|
+
if (index++ > 0) console.log("");
|
|
140
|
+
console.log(box(lines, p, state).join("\n"));
|
|
141
|
+
}
|
|
142
|
+
if (planIdStored) console.log(`\nInspect: fullstackgtm plans show ${run.planId}`);
|
|
143
|
+
console.log("Details: rerun with --verbose; machine output: --json");
|
|
144
|
+
}
|
|
145
|
+
|
|
36
146
|
function tryLoadAcquireConfig(args: string[]): EnrichConfig | undefined {
|
|
37
147
|
try {
|
|
38
148
|
const path = resolve(process.cwd(), option(args, "--config") ?? ENRICH_CONFIG_FILE_NAME);
|
|
@@ -141,8 +251,18 @@ export async function apply(args: string[]) {
|
|
|
141
251
|
const store = planId ? createFilePlanStore() : null;
|
|
142
252
|
|
|
143
253
|
if (planId && store) {
|
|
144
|
-
|
|
254
|
+
let stored = await store.get(planId);
|
|
145
255
|
if (!stored) throw new Error(`No stored plan with id ${planId}.`);
|
|
256
|
+
const reconciliation = await reconcileHostedPatchPlan(store, stored);
|
|
257
|
+
if (reconciliation.status === "conflict") throw new Error(`Refusing to apply plan ${planId}: ${reconciliation.reason}.`);
|
|
258
|
+
if (reconciliation.status === "updated") {
|
|
259
|
+
stored = reconciliation.stored;
|
|
260
|
+
console.error(`Synced hosted ${reconciliation.remoteStatus} state for ${planId}.`);
|
|
261
|
+
}
|
|
262
|
+
if (stored.status === "applied") {
|
|
263
|
+
console.log(`Plan ${planId} was already applied by another replica; imported its execution receipt. No provider writes were attempted.`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
146
266
|
if (stored.status !== "approved") {
|
|
147
267
|
throw new Error(
|
|
148
268
|
`Plan ${planId} is ${stored.status}; approve operations first with \`fullstackgtm plans approve ${planId} --operations <ids|all>\`.`,
|
|
@@ -235,9 +355,29 @@ export async function apply(args: string[]) {
|
|
|
235
355
|
// transmits nothing; a CRM provider writes records. Same governed apply path.
|
|
236
356
|
const connector = channel ? createChannelConnector(channel) : await connectorFor(provider!, args);
|
|
237
357
|
let applyClaimId: string | undefined;
|
|
358
|
+
let hostedApplyClaimId: string | undefined;
|
|
238
359
|
if (planId && store) {
|
|
239
360
|
const claimed = await store.claimApply(planId, { provider: provider ?? `channel:${channel}`, source: "cli" });
|
|
240
361
|
applyClaimId = claimed.claimId;
|
|
362
|
+
const hostedClaim = await claimHostedPlanApply(claimed.stored);
|
|
363
|
+
if (hostedClaim.status === "applied") {
|
|
364
|
+
await store.abortApplyPreflight(planId, claimed.claimId, "Another replica completed apply before provider I/O.");
|
|
365
|
+
const refreshed = await store.get(planId);
|
|
366
|
+
if (refreshed) await reconcileHostedPatchPlan(store, refreshed);
|
|
367
|
+
console.log(`Plan ${planId} was already applied by another replica. No provider writes were attempted.`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (hostedClaim.status === "conflict") {
|
|
371
|
+
await store.abortApplyPreflight(planId, claimed.claimId, `Hosted execution claim failed: ${hostedClaim.reason}`);
|
|
372
|
+
throw new Error(`Refusing to apply plan ${planId}: could not coordinate with its hosted replica (${hostedClaim.reason}).`);
|
|
373
|
+
}
|
|
374
|
+
if (hostedClaim.status === "unavailable") {
|
|
375
|
+
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.`);
|
|
376
|
+
}
|
|
377
|
+
if (hostedClaim.status === "claimed") {
|
|
378
|
+
hostedApplyClaimId = hostedClaim.claimId;
|
|
379
|
+
await store.recordHostedClaim(planId, claimed.claimId, hostedClaim.claimId);
|
|
380
|
+
}
|
|
241
381
|
const claimedVerification = verifyApprovalDigests(
|
|
242
382
|
claimed.stored.plan.operations,
|
|
243
383
|
claimed.stored.approvedOperationIds,
|
|
@@ -245,6 +385,16 @@ export async function apply(args: string[]) {
|
|
|
245
385
|
claimed.stored.approvalDigests,
|
|
246
386
|
);
|
|
247
387
|
if (!claimedVerification.ok) {
|
|
388
|
+
if (hostedApplyClaimId) {
|
|
389
|
+
const released = await releaseHostedPlanApply(
|
|
390
|
+
claimed.stored,
|
|
391
|
+
hostedApplyClaimId,
|
|
392
|
+
"Approval integrity verification failed after claim and before provider I/O.",
|
|
393
|
+
);
|
|
394
|
+
if (released.status === "conflict" || released.status === "unavailable") {
|
|
395
|
+
console.error(`Hosted apply claim may require recovery: ${released.reason}.`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
248
398
|
await store.abortApplyPreflight(
|
|
249
399
|
planId,
|
|
250
400
|
claimed.claimId,
|
|
@@ -280,7 +430,12 @@ export async function apply(args: string[]) {
|
|
|
280
430
|
renderer.done();
|
|
281
431
|
}
|
|
282
432
|
if (planId && store) {
|
|
283
|
-
await store.recordRun(planId, run, applyClaimId!);
|
|
433
|
+
const recorded = await store.recordRun(planId, run, applyClaimId!);
|
|
434
|
+
const durableHostedClaimId = recorded.applyAttempts?.find((attempt) => attempt.id === applyClaimId)?.hostedClaimId;
|
|
435
|
+
const mirrored = await reportHostedPlanLifecycle(recorded, { claimId: durableHostedClaimId ?? hostedApplyClaimId });
|
|
436
|
+
if (mirrored.status === "unavailable" || mirrored.status === "conflict") {
|
|
437
|
+
console.error(`Hosted plan status is stale: ${mirrored.reason}. Local apply history remains authoritative.`);
|
|
438
|
+
}
|
|
284
439
|
}
|
|
285
440
|
|
|
286
441
|
// Charge the acquire meter for the creates that actually landed.
|
|
@@ -315,8 +470,10 @@ export async function apply(args: string[]) {
|
|
|
315
470
|
|
|
316
471
|
if (args.includes("--json")) {
|
|
317
472
|
console.log(JSON.stringify(run, null, 2));
|
|
318
|
-
} else {
|
|
473
|
+
} else if (args.includes("--verbose")) {
|
|
319
474
|
console.log(formatPatchPlanRun(run));
|
|
475
|
+
} else {
|
|
476
|
+
printApplyCards(run, plan, approvedOperationIds, Boolean(planId && store));
|
|
320
477
|
}
|
|
321
478
|
if (run.status === "failed") process.exitCode = 1;
|
|
322
479
|
}
|
|
@@ -408,7 +565,13 @@ export async function plansCommand(args: string[]) {
|
|
|
408
565
|
const status = option(rest, "--status") as
|
|
409
566
|
| "draft" | "needs_approval" | "approved" | "rejected" | "applied"
|
|
410
567
|
| null;
|
|
411
|
-
|
|
568
|
+
let plans = await store.list(status ?? undefined);
|
|
569
|
+
const synced = await Promise.all(plans.map(async (stored) => {
|
|
570
|
+
const result = await reconcileHostedPatchPlan(store, stored);
|
|
571
|
+
if (result.status === "updated") return result.stored;
|
|
572
|
+
return stored;
|
|
573
|
+
}));
|
|
574
|
+
plans = status ? synced.filter((stored) => stored.status === status) : synced;
|
|
412
575
|
if (rest.includes("--json") || args.includes("--json")) {
|
|
413
576
|
console.log(
|
|
414
577
|
JSON.stringify(
|
|
@@ -432,14 +595,23 @@ export async function plansCommand(args: string[]) {
|
|
|
432
595
|
}
|
|
433
596
|
const p = paint(colorEnabled(process.stdout));
|
|
434
597
|
if (p.enabled) {
|
|
435
|
-
|
|
436
|
-
|
|
598
|
+
const statusCounts = new Map<string, number>();
|
|
599
|
+
for (const stored of plans) statusCounts.set(stored.status, (statusCounts.get(stored.status) ?? 0) + 1);
|
|
600
|
+
const awaiting = plans.filter((stored) => stored.status === "needs_approval").length;
|
|
601
|
+
const approved = plans.filter((stored) => stored.status === "approved").length;
|
|
602
|
+
console.log(box([
|
|
603
|
+
`${plans.length} plan${plans.length === 1 ? "" : "s"} · ${awaiting} awaiting approval · ${approved} ready to apply`,
|
|
604
|
+
[...statusCounts.entries()].map(([name, count]) => `${name.replaceAll("_", " ")}: ${count}`).join(" · "),
|
|
605
|
+
], p, "Change queue").join("\n"));
|
|
606
|
+
console.log("");
|
|
607
|
+
// Interactive terminals get an aligned decision table; the plain
|
|
608
|
+
// per-line format below remains stable for pipes and scripts.
|
|
437
609
|
const rows = plans.map((stored) => [
|
|
438
|
-
stored.plan.id,
|
|
439
610
|
stored.status,
|
|
440
|
-
|
|
611
|
+
stored.plan.title,
|
|
612
|
+
`${stored.approvedOperationIds.length}/${stored.plan.operations.length} selected`,
|
|
441
613
|
`${stored.runs.length} run${stored.runs.length === 1 ? "" : "s"}`,
|
|
442
|
-
stored.plan.
|
|
614
|
+
stored.plan.id,
|
|
443
615
|
]);
|
|
444
616
|
// Long summaries would wrap and break the table's alignment. The summary
|
|
445
617
|
// is the LAST column: cap it to what's left of the terminal width after
|
|
@@ -447,19 +619,26 @@ export async function plansCommand(args: string[]) {
|
|
|
447
619
|
// terminals still show something useful).
|
|
448
620
|
const columns = process.stdout.columns ?? 80;
|
|
449
621
|
const fixedWidth =
|
|
450
|
-
[0,
|
|
622
|
+
[0, 2, 3, 4].reduce(
|
|
451
623
|
(sum, index) => sum + Math.max(...rows.map((row) => row[index].length)),
|
|
452
624
|
0,
|
|
453
625
|
) + 2 * 4;
|
|
454
|
-
const
|
|
455
|
-
for (const row of rows) row[
|
|
626
|
+
const titleWidth = Math.max(24, columns - fixedWidth);
|
|
627
|
+
for (const row of rows) row[1] = truncateToWidth(row[1], titleWidth);
|
|
456
628
|
const statusPainter = (cell: string) => {
|
|
457
629
|
const painted = planStatusWord(cell.trimEnd(), p);
|
|
458
630
|
return painted + cell.slice(cell.trimEnd().length);
|
|
459
631
|
};
|
|
460
|
-
for (const line of table(rows, [
|
|
632
|
+
for (const line of table(rows, [statusPainter, null, p.dim, p.dim, p.dim])) {
|
|
461
633
|
console.log(line);
|
|
462
634
|
}
|
|
635
|
+
const next = plans.find((stored) => stored.status === "approved") ?? plans.find((stored) => stored.status === "needs_approval");
|
|
636
|
+
if (next) {
|
|
637
|
+
const command = next.status === "approved"
|
|
638
|
+
? `fullstackgtm apply --plan-id ${next.plan.id} --provider <name>`
|
|
639
|
+
: `fullstackgtm plans show ${next.plan.id}`;
|
|
640
|
+
console.log(`\nNext: ${command}`);
|
|
641
|
+
}
|
|
463
642
|
return;
|
|
464
643
|
}
|
|
465
644
|
for (const stored of plans) {
|
|
@@ -473,25 +652,34 @@ export async function plansCommand(args: string[]) {
|
|
|
473
652
|
if (subcommand === "show") {
|
|
474
653
|
const planId = rest.find((arg) => !arg.startsWith("--"));
|
|
475
654
|
if (!planId) throw new Error("Usage: fullstackgtm plans show <planId>");
|
|
476
|
-
|
|
655
|
+
let stored = await store.get(planId);
|
|
477
656
|
if (!stored) throw new Error(`No stored plan with id ${planId}.`);
|
|
657
|
+
const reconciliation = await reconcileHostedPatchPlan(store, stored);
|
|
658
|
+
if (reconciliation.status === "updated") {
|
|
659
|
+
stored = reconciliation.stored;
|
|
660
|
+
console.error(`Synced hosted ${reconciliation.remoteStatus} state for ${planId}.`);
|
|
661
|
+
} else if (reconciliation.status === "conflict") {
|
|
662
|
+
console.error(`Hosted sync conflict: ${reconciliation.reason}.`);
|
|
663
|
+
}
|
|
478
664
|
if (rest.includes("--json")) {
|
|
479
665
|
console.log(JSON.stringify(stored, null, 2));
|
|
480
666
|
return;
|
|
481
667
|
}
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
console.log(`
|
|
485
|
-
|
|
486
|
-
if (stored.applyAttempts?.length) {
|
|
668
|
+
const mirror = await uploadHostedPatchPlan(stored);
|
|
669
|
+
printPlanCards(stored, mirror.status === "saved" ? mirror.state.url : undefined);
|
|
670
|
+
if (mirror.status === "unavailable" || mirror.status === "conflict") console.log(`Hosted sync pending: ${mirror.reason}`);
|
|
671
|
+
if (rest.includes("--verbose") && stored.applyAttempts?.length) {
|
|
487
672
|
console.log("Apply attempts:");
|
|
488
673
|
for (const attempt of stored.applyAttempts) {
|
|
489
674
|
console.log(` ${attempt.id} ${attempt.status} ${attempt.provider} via ${attempt.source} ${attempt.claimedAt}`);
|
|
490
675
|
if (attempt.note) console.log(` ${attempt.note}`);
|
|
491
676
|
}
|
|
492
677
|
}
|
|
493
|
-
|
|
494
|
-
|
|
678
|
+
if (rest.includes("--verbose")) {
|
|
679
|
+
const showPaint = paint(colorEnabled(process.stdout));
|
|
680
|
+
console.log("\nFull plan document\n");
|
|
681
|
+
console.log(stylizePlanMarkdown(patchPlanToMarkdown(stored.plan), showPaint));
|
|
682
|
+
}
|
|
495
683
|
return;
|
|
496
684
|
}
|
|
497
685
|
|
|
@@ -541,6 +729,10 @@ export async function plansCommand(args: string[]) {
|
|
|
541
729
|
...fileOverrides,
|
|
542
730
|
...explicitOverrides,
|
|
543
731
|
});
|
|
732
|
+
const mirror = await reportHostedPlanLifecycle(updated);
|
|
733
|
+
if (mirror.status === "unavailable" || mirror.status === "conflict") {
|
|
734
|
+
console.error(`Hosted plan status is stale: ${mirror.reason}. Local approval remains authoritative.`);
|
|
735
|
+
}
|
|
544
736
|
console.log(
|
|
545
737
|
`Approved ${updated.approvedOperationIds.length} operation(s) on ${planId}. Apply with \`fullstackgtm apply --plan-id ${planId} --provider <name>\`.`,
|
|
546
738
|
);
|
|
@@ -550,7 +742,11 @@ export async function plansCommand(args: string[]) {
|
|
|
550
742
|
if (subcommand === "reject") {
|
|
551
743
|
const planId = rest.find((arg) => !arg.startsWith("--"));
|
|
552
744
|
if (!planId) throw new Error("Usage: fullstackgtm plans reject <planId>");
|
|
553
|
-
await store.reject(planId);
|
|
745
|
+
const rejected = await store.reject(planId);
|
|
746
|
+
const mirror = await reportHostedPlanLifecycle(rejected);
|
|
747
|
+
if (mirror.status === "unavailable" || mirror.status === "conflict") {
|
|
748
|
+
console.error(`Hosted plan status is stale: ${mirror.reason}. Local rejection remains authoritative.`);
|
|
749
|
+
}
|
|
554
750
|
console.log(`Rejected ${planId}.`);
|
|
555
751
|
return;
|
|
556
752
|
}
|
|
@@ -581,5 +777,43 @@ export async function plansCommand(args: string[]) {
|
|
|
581
777
|
return;
|
|
582
778
|
}
|
|
583
779
|
|
|
584
|
-
|
|
780
|
+
if (subcommand === "sync") {
|
|
781
|
+
const plans = await store.list();
|
|
782
|
+
let updated = 0;
|
|
783
|
+
let completed = 0;
|
|
784
|
+
const warnings: string[] = [];
|
|
785
|
+
const status = createStatusLine();
|
|
786
|
+
status.set(`Synchronizing plan replicas 0/${plans.length}`);
|
|
787
|
+
// A workspace can accumulate hundreds of local plans. Reconcile with a
|
|
788
|
+
// small worker pool so one network round-trip per plan does not turn into
|
|
789
|
+
// a minutes-long silent serial wait, without stampeding the hosted API.
|
|
790
|
+
let next = 0;
|
|
791
|
+
const worker = async () => {
|
|
792
|
+
while (next < plans.length) {
|
|
793
|
+
const stored = plans[next++];
|
|
794
|
+
const result = await reconcileHostedPatchPlan(store, stored);
|
|
795
|
+
if (result.status === "updated") updated += 1;
|
|
796
|
+
else if (result.status === "conflict" || result.status === "unavailable") warnings.push(`${stored.plan.id}: ${result.reason}`);
|
|
797
|
+
// Historical local-only plans have no remote row to receive a receipt.
|
|
798
|
+
// `backfill runs` is the explicit import path for that old history.
|
|
799
|
+
if (stored.status === "applied" && result.status !== "missing" && (stored.applyAttempts?.length ?? 0) > 0) {
|
|
800
|
+
const hostedClaimId = [...(stored.applyAttempts ?? [])].reverse().find((attempt) => attempt.hostedClaimId)?.hostedClaimId;
|
|
801
|
+
const pushed = await reportHostedPlanLifecycle(stored, { claimId: hostedClaimId });
|
|
802
|
+
if (pushed.status === "conflict" || pushed.status === "unavailable") warnings.push(`${stored.plan.id} receipt: ${pushed.reason}`);
|
|
803
|
+
}
|
|
804
|
+
completed += 1;
|
|
805
|
+
status.set(`Synchronizing plan replicas ${completed}/${plans.length}`);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
try {
|
|
809
|
+
await Promise.all(Array.from({ length: Math.min(6, plans.length) }, () => worker()));
|
|
810
|
+
} finally {
|
|
811
|
+
status.done();
|
|
812
|
+
}
|
|
813
|
+
console.log(`Plan replicas synchronized: ${updated} updated, ${plans.length - updated} unchanged.`);
|
|
814
|
+
for (const warning of warnings) console.error(` ${warning}`);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
throw unknownSubcommandError("plans", subcommand, ["list", "show", "sync", "approve", "reject", "recover"]);
|
|
585
819
|
}
|