fullstackgtm 0.50.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 +24 -0
- package/README.md +7 -0
- 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 +3 -2
- package/dist/cli/fix.js +6 -3
- package/dist/cli/help.js +22 -22
- 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 +184 -33
- package/dist/cli/schedule.js +22 -11
- package/dist/cli/signals.js +22 -3
- package/dist/cli/tam.js +10 -1
- package/dist/cli/ui.js +14 -6
- package/dist/connectors/hubspot.d.ts +4 -0
- package/dist/connectors/hubspot.js +36 -18
- package/dist/hostedPatchPlan.js +6 -1
- package/package.json +1 -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 +3 -2
- package/src/cli/fix.ts +5 -3
- package/src/cli/help.ts +22 -22
- 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 +174 -29
- package/src/cli/schedule.ts +21 -15
- package/src/cli/signals.ts +22 -3
- package/src/cli/tam.ts +8 -1
- package/src/cli/ui.ts +15 -6
- package/src/connectors/hubspot.ts +41 -17
- package/src/hostedPatchPlan.ts +6 -1
package/src/cli/backfill.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { progressReporter } from "../runReport.ts";
|
|
|
15
15
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
16
16
|
import { option, readSnapshot, saveRequested } from "./shared.ts";
|
|
17
17
|
import { colorEnabled, createProgressRenderer, paint, stylizePlanMarkdown, table } from "./ui.ts";
|
|
18
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
18
19
|
|
|
19
20
|
/** STRIPE_SECRET_KEY env ∨ stored `login stripe` credential (same ladder as `--provider stripe`). */
|
|
20
21
|
function resolveStripeKey(): string {
|
|
@@ -93,6 +94,8 @@ export async function backfillCommand(args: string[]) {
|
|
|
93
94
|
2,
|
|
94
95
|
),
|
|
95
96
|
);
|
|
97
|
+
} else if (!verbosePlanRequested(rest)) {
|
|
98
|
+
console.log(compactPlan(result.plan, { saved: save && result.plan.operations.length > 0 }));
|
|
96
99
|
} else {
|
|
97
100
|
// TTY-only styling; piped output stays byte-identical plain text.
|
|
98
101
|
console.log(
|
package/src/cli/call.ts
CHANGED
|
@@ -177,11 +177,11 @@ score always needs a key (scoring is LLM work).`);
|
|
|
177
177
|
if (!outPath) console.log(JSON.stringify(parsed, null, 2));
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
|
-
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""}
|
|
181
|
-
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
180
|
+
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""}`);
|
|
181
|
+
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights · ${parsed.summary.highImportance} high priority`);
|
|
182
|
+
const visible = rest.includes("--verbose") ? parsed.insights : parsed.insights.filter((insight) => insight.importance >= 3);
|
|
183
|
+
for (const insight of visible) console.log(`\n${insight.type.replaceAll("_", " ")} · priority ${insight.importance}\n ${insight.text.replace(/\s+/g, " ").trim()}`);
|
|
184
|
+
if (!rest.includes("--verbose") && visible.length < parsed.insights.length) console.log(`\n${parsed.insights.length - visible.length} lower-priority insight(s) hidden; use --verbose to show all.`);
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -301,13 +301,32 @@ score always needs a key (scoring is LLM work).`);
|
|
|
301
301
|
console.log(JSON.stringify(scorecard, null, 2));
|
|
302
302
|
return;
|
|
303
303
|
}
|
|
304
|
-
console.log(renderScorecard(scorecard, title));
|
|
304
|
+
console.log(rest.includes("--verbose") ? renderScorecard(scorecard, title) : renderCompactScorecard(scorecard, title));
|
|
305
305
|
return;
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
throw new Error(`call supports: parse, classify, link, plan, score (got ${subcommand ?? "nothing"})`);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
function renderCompactScorecard(scorecard: CallScorecard, title?: string): string {
|
|
312
|
+
const lines = [
|
|
313
|
+
`${title ?? "Call"} · ${scorecard.overallScore}/${scorecard.scale}${scorecard.band ? ` · ${scorecard.band.label}` : ""}`,
|
|
314
|
+
scorecard.rubricName ? `${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "",
|
|
315
|
+
"",
|
|
316
|
+
].filter((line, index) => line || index === 2);
|
|
317
|
+
for (const dimension of scorecard.dimensions) {
|
|
318
|
+
lines.push(`${dimension.score}/${dimension.maxScore} ${dimension.name}`);
|
|
319
|
+
lines.push(` ${dimension.coachingNote.replace(/\s+/g, " ").trim()}`);
|
|
320
|
+
}
|
|
321
|
+
if (scorecard.missedItems.length) {
|
|
322
|
+
lines.push("", "Focus next");
|
|
323
|
+
for (const item of scorecard.missedItems) lines.push(` ${item}`);
|
|
324
|
+
}
|
|
325
|
+
if (scorecard.highlights.length) lines.push("", `Strengths: ${scorecard.highlights.join(" · ")}`);
|
|
326
|
+
lines.push("", "Use --verbose for the full coaching scorecard.");
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
311
330
|
function renderScorecard(scorecard: CallScorecard, title?: string): string {
|
|
312
331
|
const bandText = scorecard.band ? ` — ${scorecard.band.label}` : "";
|
|
313
332
|
const rubricLine = scorecard.rubricName ? `Rubric: ${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "";
|
package/src/cli/draft.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { createFileJudgeStore } from "../judge.ts";
|
|
|
9
9
|
import { authorOpeners, DEFAULT_DRAFT_PROMPT, DRAFT_CHANNELS, draft, type DraftChannel } from "../draft.ts";
|
|
10
10
|
import type { LlmCallOptions } from "../llm.ts";
|
|
11
11
|
import { numericOption, option, resolveLlmBaseUrls, saveRequested } from "./shared.ts";
|
|
12
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -78,7 +79,14 @@ LLM key it emits a clearly-labeled stub rather than fake authored copy.`);
|
|
|
78
79
|
openers,
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
if (args.includes("--json")) {
|
|
83
|
+
console.log(JSON.stringify({ drafts, rejected }, null, 2));
|
|
84
|
+
} else if (verbosePlanRequested(args)) {
|
|
85
|
+
console.log(JSON.stringify({ drafts, rejected }, null, 2));
|
|
86
|
+
console.log(compactPlan(plan, { saved: save }));
|
|
87
|
+
} else {
|
|
88
|
+
console.log(compactPlan(plan, { saved: save }));
|
|
89
|
+
}
|
|
82
90
|
const stale = drafts.filter((d) => d.staleTrigger);
|
|
83
91
|
console.error(
|
|
84
92
|
`${drafts.length} opener(s) staged as create_task proposals (channel ${channel}, min score ${minScore})` +
|
package/src/cli/enrich.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { isOptionValue, loadIcp, numericOption, option, readSnapshot, saveReques
|
|
|
24
24
|
import { providerKey } from "./tam.ts";
|
|
25
25
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
26
26
|
import { colorEnabled, createProgressRenderer, createStatusLine, formatBar, formatDuration, paint, type Paint } from "./ui.ts";
|
|
27
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
27
28
|
import type { AcquireBudget } from "../acquireMeter.ts";
|
|
28
29
|
|
|
29
30
|
|
|
@@ -309,7 +310,7 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
309
310
|
),
|
|
310
311
|
);
|
|
311
312
|
} else {
|
|
312
|
-
console.log(patchPlanToMarkdown(result.plan));
|
|
313
|
+
console.log(verbosePlanRequested(rest) ? patchPlanToMarkdown(result.plan) : compactPlan(result.plan));
|
|
313
314
|
console.log(meterLine);
|
|
314
315
|
if (gaugeLine) console.log(gaugeLine);
|
|
315
316
|
console.log("\nDry run — nothing written. Re-run with --save to persist the plan, then approve + apply.");
|
|
@@ -514,7 +515,7 @@ are phase 2. Recurring execution is the scheduler's job; enrich has no cron.`);
|
|
|
514
515
|
if (rest.includes("--json")) {
|
|
515
516
|
console.log(JSON.stringify(result.plan, null, 2));
|
|
516
517
|
} else {
|
|
517
|
-
console.log(patchPlanToMarkdown(result.plan));
|
|
518
|
+
console.log(verbosePlanRequested(rest) ? patchPlanToMarkdown(result.plan) : compactPlan(result.plan));
|
|
518
519
|
console.log(formatEnrichCounts(result.counts, result.ambiguities.length));
|
|
519
520
|
console.log("\nDry run — nothing written. Re-run with --save to persist the plan and the run record.");
|
|
520
521
|
}
|
package/src/cli/fix.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { progressReporter } from "../runReport.ts";
|
|
|
22
22
|
import type { PatchPlan, PatchPlanRun } from "../types.ts";
|
|
23
23
|
import { confirmRequested, connectorFor, isOptionValue, numericOption, option, readSnapshot, repeatedOption, saveRequested } from "./shared.ts";
|
|
24
24
|
import { box, colorEnabled, createProgressRenderer, paint } from "./ui.ts";
|
|
25
|
+
import { compactPlan, verbosePlanRequested } from "./planOutput.ts";
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -107,8 +108,10 @@ async function emitPlan(plan: PatchPlan, args: string[]) {
|
|
|
107
108
|
}
|
|
108
109
|
if (args.includes("--json")) {
|
|
109
110
|
console.log(JSON.stringify(plan, null, 2));
|
|
110
|
-
} else {
|
|
111
|
+
} else if (verbosePlanRequested(args)) {
|
|
111
112
|
console.log(patchPlanToMarkdown(plan));
|
|
113
|
+
} else {
|
|
114
|
+
console.log(compactPlan(plan, { saved: saveRequested(args) }));
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
|
|
@@ -181,8 +184,7 @@ export async function reassignCommand(args: string[]) {
|
|
|
181
184
|
const store = saveRequested(args) ? createFilePlanStore() : null;
|
|
182
185
|
for (const plan of plans) {
|
|
183
186
|
if (store) await store.save(plan);
|
|
184
|
-
console.log(
|
|
185
|
-
console.log(` ${plan.summary}`);
|
|
187
|
+
console.log(verbosePlanRequested(args) ? patchPlanToMarkdown(plan) : compactPlan(plan, { saved: Boolean(store) }));
|
|
186
188
|
}
|
|
187
189
|
if (store) {
|
|
188
190
|
console.log(
|
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> | sync | 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
|
|
@@ -550,7 +550,7 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
550
550
|
summary: "replicated plan lifecycle: list / show / sync / approve / reject",
|
|
551
551
|
phase: "Govern",
|
|
552
552
|
synopsis: [
|
|
553
|
-
"fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>",
|
|
553
|
+
"fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>",
|
|
554
554
|
"fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
|
|
555
555
|
"fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
|
|
556
556
|
"fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
|
|
@@ -563,12 +563,12 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
563
563
|
summary: "write ONLY explicitly approved operations to a provider",
|
|
564
564
|
phase: "Govern / Verify",
|
|
565
565
|
synopsis: [
|
|
566
|
-
"fullstackgtm apply --plan-id <id> --provider <name>",
|
|
566
|
+
"fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]",
|
|
567
567
|
"fullstackgtm apply --plan-id <id> --channel outbox (render approved drafted openers to the outbox; transmits nothing)",
|
|
568
568
|
"fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [--value <opId>=<v>]",
|
|
569
569
|
],
|
|
570
570
|
detail:
|
|
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. 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.",
|
|
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.",
|
|
572
572
|
seeAlso: ["plans", "suggest", "audit-log"],
|
|
573
573
|
},
|
|
574
574
|
"audit-log": {
|
|
@@ -658,7 +658,7 @@ export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedul
|
|
|
658
658
|
export const GLOBAL_FLAGS = ["--help", "--full"];
|
|
659
659
|
export const GLOBAL_SHORT_FLAGS = ["-h"];
|
|
660
660
|
export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
|
|
661
|
-
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"];
|
|
662
662
|
|
|
663
663
|
// Complete per-command flag registry used by runCli's fail-closed flag
|
|
664
664
|
// validation. Keep this next to HELP so focused help, machine capabilities,
|
|
@@ -667,7 +667,7 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
667
667
|
init: ["--source", "--provider", "--out", "--force"],
|
|
668
668
|
login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
|
|
669
669
|
logout: [],
|
|
670
|
-
doctor: ["--json"],
|
|
670
|
+
doctor: ["--verbose", "--json"],
|
|
671
671
|
capabilities: ["--json"],
|
|
672
672
|
"robot-docs": [],
|
|
673
673
|
profiles: ["--json"],
|
|
@@ -681,24 +681,24 @@ export const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
681
681
|
hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
|
|
682
682
|
relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
|
|
683
683
|
route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
684
|
-
fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
|
|
685
|
-
"bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
686
|
-
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
687
|
-
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
688
|
-
backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--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", "--json", "--out"],
|
|
690
|
-
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"],
|
|
691
691
|
suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
|
|
692
|
-
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
|
|
693
|
-
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"],
|
|
694
694
|
"audit-log": ["--in", "--out", "--json"],
|
|
695
695
|
merge: ["--input", "--out", "--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", "--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", "--json", "--out"],
|
|
698
|
-
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
|
|
699
|
-
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
|
|
700
|
-
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
|
|
701
|
-
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--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"],
|
|
702
702
|
};
|
|
703
703
|
|
|
704
704
|
export const FLAGS_WITH_VALUES = new Set([
|
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,7 +11,7 @@ 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";
|
|
@@ -20,7 +20,7 @@ import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progre
|
|
|
20
20
|
import type { ValueSuggestion } from "../suggest.ts";
|
|
21
21
|
import type { CanonicalGtmSnapshot, CreateRecordPayload, PatchOperation, PatchPlan, PatchPlanRun } from "../types.ts";
|
|
22
22
|
import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.ts";
|
|
23
|
-
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";
|
|
24
24
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
25
25
|
|
|
26
26
|
|
|
@@ -34,6 +34,115 @@ function parseValueOverrides(args: string[]) {
|
|
|
34
34
|
return valueOverrides;
|
|
35
35
|
}
|
|
36
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
|
+
|
|
37
146
|
function tryLoadAcquireConfig(args: string[]): EnrichConfig | undefined {
|
|
38
147
|
try {
|
|
39
148
|
const path = resolve(process.cwd(), option(args, "--config") ?? ENRICH_CONFIG_FILE_NAME);
|
|
@@ -361,8 +470,10 @@ export async function apply(args: string[]) {
|
|
|
361
470
|
|
|
362
471
|
if (args.includes("--json")) {
|
|
363
472
|
console.log(JSON.stringify(run, null, 2));
|
|
364
|
-
} else {
|
|
473
|
+
} else if (args.includes("--verbose")) {
|
|
365
474
|
console.log(formatPatchPlanRun(run));
|
|
475
|
+
} else {
|
|
476
|
+
printApplyCards(run, plan, approvedOperationIds, Boolean(planId && store));
|
|
366
477
|
}
|
|
367
478
|
if (run.status === "failed") process.exitCode = 1;
|
|
368
479
|
}
|
|
@@ -484,14 +595,23 @@ export async function plansCommand(args: string[]) {
|
|
|
484
595
|
}
|
|
485
596
|
const p = paint(colorEnabled(process.stdout));
|
|
486
597
|
if (p.enabled) {
|
|
487
|
-
|
|
488
|
-
|
|
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.
|
|
489
609
|
const rows = plans.map((stored) => [
|
|
490
|
-
stored.plan.id,
|
|
491
610
|
stored.status,
|
|
492
|
-
|
|
611
|
+
stored.plan.title,
|
|
612
|
+
`${stored.approvedOperationIds.length}/${stored.plan.operations.length} selected`,
|
|
493
613
|
`${stored.runs.length} run${stored.runs.length === 1 ? "" : "s"}`,
|
|
494
|
-
stored.plan.
|
|
614
|
+
stored.plan.id,
|
|
495
615
|
]);
|
|
496
616
|
// Long summaries would wrap and break the table's alignment. The summary
|
|
497
617
|
// is the LAST column: cap it to what's left of the terminal width after
|
|
@@ -499,19 +619,26 @@ export async function plansCommand(args: string[]) {
|
|
|
499
619
|
// terminals still show something useful).
|
|
500
620
|
const columns = process.stdout.columns ?? 80;
|
|
501
621
|
const fixedWidth =
|
|
502
|
-
[0,
|
|
622
|
+
[0, 2, 3, 4].reduce(
|
|
503
623
|
(sum, index) => sum + Math.max(...rows.map((row) => row[index].length)),
|
|
504
624
|
0,
|
|
505
625
|
) + 2 * 4;
|
|
506
|
-
const
|
|
507
|
-
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);
|
|
508
628
|
const statusPainter = (cell: string) => {
|
|
509
629
|
const painted = planStatusWord(cell.trimEnd(), p);
|
|
510
630
|
return painted + cell.slice(cell.trimEnd().length);
|
|
511
631
|
};
|
|
512
|
-
for (const line of table(rows, [
|
|
632
|
+
for (const line of table(rows, [statusPainter, null, p.dim, p.dim, p.dim])) {
|
|
513
633
|
console.log(line);
|
|
514
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
|
+
}
|
|
515
642
|
return;
|
|
516
643
|
}
|
|
517
644
|
for (const stored of plans) {
|
|
@@ -538,22 +665,21 @@ export async function plansCommand(args: string[]) {
|
|
|
538
665
|
console.log(JSON.stringify(stored, null, 2));
|
|
539
666
|
return;
|
|
540
667
|
}
|
|
541
|
-
const showPaint = paint(colorEnabled(process.stdout));
|
|
542
|
-
console.log(`Status: ${planStatusWord(stored.status, showPaint)}`);
|
|
543
|
-
console.log(`Approved operations: ${stored.approvedOperationIds.join(", ") || "none"}`);
|
|
544
|
-
console.log(`Runs: ${stored.runs.length}`);
|
|
545
668
|
const mirror = await uploadHostedPatchPlan(stored);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (stored.applyAttempts?.length) {
|
|
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) {
|
|
549
672
|
console.log("Apply attempts:");
|
|
550
673
|
for (const attempt of stored.applyAttempts) {
|
|
551
674
|
console.log(` ${attempt.id} ${attempt.status} ${attempt.provider} via ${attempt.source} ${attempt.claimedAt}`);
|
|
552
675
|
if (attempt.note) console.log(` ${attempt.note}`);
|
|
553
676
|
}
|
|
554
677
|
}
|
|
555
|
-
|
|
556
|
-
|
|
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
|
+
}
|
|
557
683
|
return;
|
|
558
684
|
}
|
|
559
685
|
|
|
@@ -654,16 +780,35 @@ export async function plansCommand(args: string[]) {
|
|
|
654
780
|
if (subcommand === "sync") {
|
|
655
781
|
const plans = await store.list();
|
|
656
782
|
let updated = 0;
|
|
783
|
+
let completed = 0;
|
|
657
784
|
const warnings: string[] = [];
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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}`);
|
|
666
806
|
}
|
|
807
|
+
};
|
|
808
|
+
try {
|
|
809
|
+
await Promise.all(Array.from({ length: Math.min(6, plans.length) }, () => worker()));
|
|
810
|
+
} finally {
|
|
811
|
+
status.done();
|
|
667
812
|
}
|
|
668
813
|
console.log(`Plan replicas synchronized: ${updated} updated, ${plans.length - updated} unchanged.`);
|
|
669
814
|
for (const warning of warnings) console.error(` ${warning}`);
|