fullstackgtm 0.44.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +293 -1
  2. package/INSTALL_FOR_AGENTS.md +13 -11
  3. package/README.md +45 -24
  4. package/dist/audit.d.ts +3 -1
  5. package/dist/audit.js +29 -2
  6. package/dist/backfill.d.ts +86 -0
  7. package/dist/backfill.js +251 -0
  8. package/dist/bin.js +4 -1
  9. package/dist/cli/audit.d.ts +15 -0
  10. package/dist/cli/audit.js +392 -0
  11. package/dist/cli/auth.d.ts +82 -0
  12. package/dist/cli/auth.js +607 -0
  13. package/dist/cli/backfill.d.ts +1 -0
  14. package/dist/cli/backfill.js +125 -0
  15. package/dist/cli/backfillRuns.d.ts +1 -0
  16. package/dist/cli/backfillRuns.js +187 -0
  17. package/dist/cli/call.d.ts +1 -0
  18. package/dist/cli/call.js +387 -0
  19. package/dist/cli/capabilities.d.ts +30 -0
  20. package/dist/cli/capabilities.js +197 -0
  21. package/dist/cli/draft.d.ts +7 -0
  22. package/dist/cli/draft.js +87 -0
  23. package/dist/cli/enrich.d.ts +8 -0
  24. package/dist/cli/enrich.js +806 -0
  25. package/dist/cli/fix.d.ts +33 -0
  26. package/dist/cli/fix.js +346 -0
  27. package/dist/cli/help.d.ts +25 -0
  28. package/dist/cli/help.js +646 -0
  29. package/dist/cli/icp.d.ts +7 -0
  30. package/dist/cli/icp.js +229 -0
  31. package/dist/cli/init.d.ts +7 -0
  32. package/dist/cli/init.js +58 -0
  33. package/dist/cli/market.d.ts +1 -0
  34. package/dist/cli/market.js +391 -0
  35. package/dist/cli/plans.d.ts +5 -0
  36. package/dist/cli/plans.js +456 -0
  37. package/dist/cli/schedule.d.ts +8 -0
  38. package/dist/cli/schedule.js +479 -0
  39. package/dist/cli/shared.d.ts +71 -0
  40. package/dist/cli/shared.js +342 -0
  41. package/dist/cli/signals.d.ts +7 -0
  42. package/dist/cli/signals.js +412 -0
  43. package/dist/cli/suggest.d.ts +49 -0
  44. package/dist/cli/suggest.js +135 -0
  45. package/dist/cli/tam.d.ts +9 -0
  46. package/dist/cli/tam.js +387 -0
  47. package/dist/cli/ui.d.ts +142 -0
  48. package/dist/cli/ui.js +427 -0
  49. package/dist/cli.d.ts +1 -57
  50. package/dist/cli.js +123 -5157
  51. package/dist/connector.d.ts +23 -0
  52. package/dist/connector.js +47 -0
  53. package/dist/connectors/hubspot.d.ts +10 -1
  54. package/dist/connectors/hubspot.js +244 -10
  55. package/dist/connectors/hubspotAuth.js +5 -1
  56. package/dist/connectors/linkedin.js +14 -14
  57. package/dist/connectors/salesforce.d.ts +10 -1
  58. package/dist/connectors/salesforce.js +39 -6
  59. package/dist/connectors/signalSources.js +7 -59
  60. package/dist/connectors/stripe.d.ts +37 -0
  61. package/dist/connectors/stripe.js +103 -31
  62. package/dist/enrich.d.ts +7 -0
  63. package/dist/enrich.js +7 -0
  64. package/dist/health.d.ts +11 -69
  65. package/dist/health.js +4 -134
  66. package/dist/healthScore.d.ts +71 -0
  67. package/dist/healthScore.js +143 -0
  68. package/dist/icp.d.ts +15 -11
  69. package/dist/icp.js +19 -36
  70. package/dist/index.d.ts +3 -1
  71. package/dist/index.js +3 -1
  72. package/dist/judge.d.ts +2 -0
  73. package/dist/judge.js +6 -0
  74. package/dist/llm.d.ts +29 -0
  75. package/dist/llm.js +206 -0
  76. package/dist/market.d.ts +39 -1
  77. package/dist/market.js +116 -44
  78. package/dist/marketClassify.d.ts +11 -1
  79. package/dist/marketClassify.js +17 -2
  80. package/dist/marketTaxonomy.d.ts +6 -1
  81. package/dist/marketTaxonomy.js +4 -2
  82. package/dist/mcp-bin.js +31 -2
  83. package/dist/mcp.js +372 -166
  84. package/dist/progress.d.ts +96 -0
  85. package/dist/progress.js +142 -0
  86. package/dist/runReport.d.ts +24 -0
  87. package/dist/runReport.js +139 -4
  88. package/dist/schedule.d.ts +80 -2
  89. package/dist/schedule.js +254 -1
  90. package/dist/spoolFiles.d.ts +44 -0
  91. package/dist/spoolFiles.js +114 -0
  92. package/dist/types.d.ts +29 -1
  93. package/docs/api.md +75 -8
  94. package/docs/architecture.md +2 -0
  95. package/docs/linkedin-connector-spec.md +1 -1
  96. package/docs/signal-spool-format.md +13 -0
  97. package/llms.txt +18 -9
  98. package/package.json +10 -3
  99. package/skills/fullstackgtm/SKILL.md +2 -1
  100. package/src/audit.ts +27 -1
  101. package/src/backfill.ts +340 -0
  102. package/src/bin.ts +5 -1
  103. package/src/cli/audit.ts +447 -0
  104. package/src/cli/auth.ts +669 -0
  105. package/src/cli/backfill.ts +156 -0
  106. package/src/cli/backfillRuns.ts +198 -0
  107. package/src/cli/call.ts +414 -0
  108. package/src/cli/capabilities.ts +215 -0
  109. package/src/cli/draft.ts +101 -0
  110. package/src/cli/enrich.ts +908 -0
  111. package/src/cli/fix.ts +373 -0
  112. package/src/cli/help.ts +702 -0
  113. package/src/cli/icp.ts +265 -0
  114. package/src/cli/init.ts +65 -0
  115. package/src/cli/market.ts +423 -0
  116. package/src/cli/plans.ts +524 -0
  117. package/src/cli/schedule.ts +526 -0
  118. package/src/cli/shared.ts +392 -0
  119. package/src/cli/signals.ts +434 -0
  120. package/src/cli/suggest.ts +151 -0
  121. package/src/cli/tam.ts +435 -0
  122. package/src/cli/ui.ts +497 -0
  123. package/src/cli.ts +122 -5855
  124. package/src/connector.ts +66 -0
  125. package/src/connectors/hubspot.ts +273 -9
  126. package/src/connectors/hubspotAuth.ts +5 -1
  127. package/src/connectors/linkedin.ts +14 -14
  128. package/src/connectors/salesforce.ts +51 -3
  129. package/src/connectors/signalSources.ts +7 -56
  130. package/src/connectors/stripe.ts +154 -34
  131. package/src/enrich.ts +13 -0
  132. package/src/health.ts +14 -213
  133. package/src/healthScore.ts +223 -0
  134. package/src/icp.ts +19 -35
  135. package/src/index.ts +28 -1
  136. package/src/judge.ts +7 -0
  137. package/src/llm.ts +239 -0
  138. package/src/market.ts +157 -44
  139. package/src/marketClassify.ts +26 -2
  140. package/src/marketTaxonomy.ts +10 -2
  141. package/src/mcp-bin.ts +34 -2
  142. package/src/mcp.ts +270 -63
  143. package/src/progress.ts +197 -0
  144. package/src/runReport.ts +159 -4
  145. package/src/schedule.ts +310 -3
  146. package/src/spoolFiles.ts +116 -0
  147. package/src/types.ts +32 -2
  148. package/docs/dx-punch-list.md +0 -87
  149. package/docs/roadmap-to-1.0.md +0 -211
@@ -0,0 +1,387 @@
1
+ // Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { patchPlanToMarkdown } from "../format.js";
5
+ import { createFilePlanStore } from "../planStore.js";
6
+ import { normalizeTranscript, parseCall, suggestCallDeal } from "../calls.js";
7
+ import { classifyCall, rubricForCallType, rubricPresets, CALL_TYPES, CALL_TYPE_IDS } from "../callTypes.js";
8
+ import { DEFAULT_RUBRIC, classifyCallLlm, extractInsightsChunked, parseRubric, resolveLlmCredential, scoreCallLlm } from "../llm.js";
9
+ import { option, readSnapshot, requireLlmCredential, saveRequested } from "./shared.js";
10
+ import { startElapsedStatus } from "./ui.js";
11
+ export async function callCommand(args) {
12
+ const [subcommand, ...rest] = args;
13
+ if (args.includes("--help") || args.includes("-h")) {
14
+ console.log(`call parse --transcript <file> [--title t] [--source s] [--model m] [--llm] [--heuristics] [--json|--ndjson] [--out <path>]
15
+ call classify --transcript <file>|--call <parsed.json> [--llm] [--deterministic] [--json] [--list]
16
+ call score --transcript <file>|--call <parsed.json> [--call-type <t>] [--rubric <rubric.json>] [--model m] [--json|--out <path>] [--list-rubrics]
17
+ call link --attendees <a@x.com,...> | --domain <x.com> [source options] [--json]
18
+ call plan --transcript <file>|--call <parsed.json> --deal <id> [source options] [--save|--json]
19
+
20
+ classify picks the call type (deterministic signals; --llm for a model tiebreak).
21
+ score auto-selects the type-specific rubric from that classification unless you
22
+ pass --call-type or --rubric. Call types: ${CALL_TYPE_IDS.join(", ")}.
23
+
24
+ parse defaults to chunked LLM extraction when a key resolves (ANTHROPIC_API_KEY/
25
+ OPENAI_API_KEY or \`login anthropic|openai\`) and falls back to the free
26
+ deterministic engine when none does. --heuristics (alias --deterministic)
27
+ forces the deterministic engine; --llm forces the LLM path (one-time key prompt
28
+ on a TTY). Model: --model, else env FSGTM_INSIGHTS_MODEL, else the provider
29
+ default. classify --deterministic needs no key.
30
+ score always needs a key (scoring is LLM work).`);
31
+ return;
32
+ }
33
+ const loadParsedCall = async () => {
34
+ const callPath = option(rest, "--call");
35
+ if (callPath) {
36
+ return JSON.parse(readFileSync(resolve(process.cwd(), callPath), "utf8"));
37
+ }
38
+ const transcriptPath = option(rest, "--transcript");
39
+ if (!transcriptPath)
40
+ throw new Error(`call ${subcommand} requires --transcript <file> or --call <parsed.json>`);
41
+ const raw = readFileSync(resolve(process.cwd(), transcriptPath), "utf8");
42
+ const source = option(rest, "--source");
43
+ const base = {
44
+ title: option(rest, "--title") ?? undefined,
45
+ sourceSystem: source,
46
+ capturedAt: new Date().toISOString(),
47
+ };
48
+ if (rest.includes("--heuristics") || rest.includes("--deterministic")) {
49
+ return parseCall(raw, base);
50
+ }
51
+ // Engine parity with the hosted app: the chunked LLM pipeline runs by
52
+ // default whenever a key resolves (env or stored login), and the free
53
+ // deterministic engine runs when none does. --llm keeps the strict path
54
+ // for scripts (one-time TTY key prompt / actionable error, never silent).
55
+ if (!rest.includes("--llm") && !resolveLlmCredential()) {
56
+ console.error("No LLM key — using deterministic extraction. `fullstackgtm login anthropic|openai` or set ANTHROPIC_API_KEY/OPENAI_API_KEY for chunked LLM insights.");
57
+ return parseCall(raw, base);
58
+ }
59
+ const credential = await requireLlmCredential();
60
+ const normalized = normalizeTranscript(raw);
61
+ // Elapsed-time spinner on interactive terminals while the model works.
62
+ const wait = startElapsedStatus((elapsed) => `Extracting insights · ${credential.provider} · ${elapsed}`);
63
+ let extraction;
64
+ try {
65
+ // Chunked langextract-style pipeline: small per-chunk extraction calls
66
+ // beat one whole-transcript pass on long calls (single-shot truncates).
67
+ extraction = await extractInsightsChunked(normalized, {
68
+ ...credential,
69
+ // --model wins, then the hosted app's env contract
70
+ // (FSGTM_INSIGHTS_MODEL), then the provider default downstream.
71
+ model: option(rest, "--model") ?? (process.env.FSGTM_INSIGHTS_MODEL || undefined),
72
+ title: base.title,
73
+ });
74
+ }
75
+ finally {
76
+ wait.done();
77
+ }
78
+ const { insights, model } = extraction;
79
+ return parseCall(raw, {
80
+ ...base,
81
+ insights,
82
+ extractor: `llm:${credential.provider}:${model}`,
83
+ });
84
+ };
85
+ // Reconstruct plain transcript text from either a --transcript file (any
86
+ // dialect, normalized) or a parsed --call JSON. Shared by classify + score.
87
+ const loadTranscriptText = () => {
88
+ const transcriptPath = option(rest, "--transcript");
89
+ if (transcriptPath) {
90
+ return normalizeTranscript(readFileSync(resolve(process.cwd(), transcriptPath), "utf8"));
91
+ }
92
+ const callPath = option(rest, "--call");
93
+ if (!callPath)
94
+ throw new Error(`call ${subcommand} requires --transcript <file> or --call <parsed.json>`);
95
+ const parsed = JSON.parse(readFileSync(resolve(process.cwd(), callPath), "utf8"));
96
+ return parsed.segments.map((s) => (s.speaker ? `${s.speaker}: ${s.text}` : s.text)).join("\n");
97
+ };
98
+ if (subcommand === "classify") {
99
+ if (rest.includes("--list")) {
100
+ const lines = CALL_TYPES.map((d) => `${d.id.padEnd(22)} ${d.name} — ${d.definition}`);
101
+ console.log(rest.includes("--json") ? JSON.stringify(CALL_TYPES, null, 2) : lines.join("\n"));
102
+ return;
103
+ }
104
+ const transcriptText = loadTranscriptText();
105
+ const title = option(rest, "--title") ?? undefined;
106
+ const deterministic = classifyCall(transcriptText);
107
+ // LLM tiebreak: explicit --llm, or auto when the deterministic pass is unsure
108
+ // and a key is available (never required — deterministic always answers).
109
+ const wantLlm = rest.includes("--llm") || (!rest.includes("--deterministic") && deterministic.confidence !== "high" && Boolean(resolveLlmCredential()));
110
+ let result = deterministic;
111
+ if (wantLlm) {
112
+ const credential = await requireLlmCredential("score");
113
+ const llm = await classifyCallLlm(transcriptText, CALL_TYPES, {
114
+ ...credential,
115
+ model: option(rest, "--model") ?? undefined,
116
+ title,
117
+ });
118
+ result = { type: llm.type, confidence: "high", reason: llm.reason, method: "llm", model: llm.model };
119
+ }
120
+ if (rest.includes("--json")) {
121
+ console.log(JSON.stringify(result, null, 2));
122
+ return;
123
+ }
124
+ const def = CALL_TYPES.find((d) => d.id === result.type);
125
+ console.log(`Call type: ${def?.name ?? result.type} (${result.type})`);
126
+ console.log(`Confidence: ${result.confidence} · via ${result.method}${result.model ? ` (${result.model})` : ""}`);
127
+ console.log(`Why: ${result.reason}`);
128
+ if (result.method === "deterministic" && deterministic.candidates.length > 1) {
129
+ const others = deterministic.candidates.slice(1, 4).map((c) => `${c.type} (${c.score})`).join(", ");
130
+ console.log(`Other matches: ${others}`);
131
+ }
132
+ console.log(`\nScore it with this rubric: fullstackgtm call score ${option(rest, "--transcript") ? `--transcript ${option(rest, "--transcript")}` : `--call ${option(rest, "--call")}`} --call-type ${result.type}`);
133
+ return;
134
+ }
135
+ if (subcommand === "parse") {
136
+ const parsed = await loadParsedCall();
137
+ const outPath = option(rest, "--out");
138
+ if (outPath)
139
+ writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(parsed, null, 2)}\n`);
140
+ if (rest.includes("--ndjson")) {
141
+ // One flat row per insight — warehouse-friendly (e.g. Snowflake COPY).
142
+ for (const insight of parsed.insights) {
143
+ console.log(JSON.stringify({
144
+ call_id: parsed.id,
145
+ call_title: parsed.title ?? null,
146
+ source_system: parsed.sourceSystem,
147
+ extractor: parsed.extractor,
148
+ type: insight.type,
149
+ title: insight.title,
150
+ text: insight.text,
151
+ evidence: insight.evidence,
152
+ speaker: insight.speaker ?? null,
153
+ confidence: insight.confidence,
154
+ importance: insight.importance,
155
+ }));
156
+ }
157
+ return;
158
+ }
159
+ if (rest.includes("--json") || outPath) {
160
+ if (!outPath)
161
+ console.log(JSON.stringify(parsed, null, 2));
162
+ return;
163
+ }
164
+ console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""} (${parsed.sourceSystem})`);
165
+ console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights (${parsed.summary.highImportance} high-importance)\n`);
166
+ for (const insight of parsed.insights) {
167
+ console.log(`[${insight.type}] (importance ${insight.importance}) ${insight.text}`);
168
+ }
169
+ return;
170
+ }
171
+ if (subcommand === "link") {
172
+ const attendees = option(rest, "--attendees");
173
+ const domain = option(rest, "--domain");
174
+ if (!attendees && !domain)
175
+ throw new Error("call link requires --attendees <emails,comma-separated> and/or --domain <example.com>");
176
+ const snapshot = await readSnapshot(rest);
177
+ const suggestion = suggestCallDeal(snapshot, {
178
+ attendeeEmails: attendees?.split(",").map((e) => e.trim()).filter(Boolean),
179
+ domain: domain ?? undefined,
180
+ });
181
+ if (rest.includes("--json")) {
182
+ console.log(JSON.stringify(suggestion, null, 2));
183
+ }
184
+ else {
185
+ const marker = suggestion.confidence === "high" ? "✓" : suggestion.confidence === "low" ? "~" : "✗";
186
+ console.log(`${marker} [${suggestion.confidence}] ${suggestion.dealId ?? "no match"}${suggestion.dealName ? ` — ${suggestion.dealName}` : ""}`);
187
+ console.log(` ${suggestion.reason}`);
188
+ }
189
+ if (suggestion.confidence === "none")
190
+ process.exitCode = 1;
191
+ return;
192
+ }
193
+ if (subcommand === "plan") {
194
+ const dealId = option(rest, "--deal");
195
+ if (!dealId)
196
+ throw new Error("call plan requires --deal <dealId> (use `call link` to find it)");
197
+ const parsed = await loadParsedCall();
198
+ const snapshot = await readSnapshot(rest);
199
+ const deal = snapshot.deals.find((row) => row.id === dealId);
200
+ if (!deal)
201
+ throw new Error(`Deal ${dealId} is not in the snapshot — check the id or the snapshot source.`);
202
+ const nextSteps = parsed.insights.filter((insight) => insight.type === "next_step");
203
+ if (nextSteps.length === 0) {
204
+ console.log("No next-step insights in this call — nothing to plan. (Other insight types are evidence, not writes.)");
205
+ return;
206
+ }
207
+ const [top, ...others] = nextSteps;
208
+ const proposed = top.text.trim().slice(0, 255);
209
+ const current = deal.nextStep?.trim() ?? "";
210
+ const plan = buildCallPlan(parsed, deal, proposed, current, others.slice(0, 3));
211
+ if (saveRequested(rest)) {
212
+ await createFilePlanStore().save(plan);
213
+ console.log(`Saved plan ${plan.id}. Review with \`fullstackgtm plans show ${plan.id}\`, approve with \`fullstackgtm plans approve ${plan.id} --operations <ids|all>\`, then \`fullstackgtm apply --plan-id ${plan.id} --provider <name>\`.`);
214
+ return;
215
+ }
216
+ console.log(rest.includes("--json") ? JSON.stringify(plan, null, 2) : patchPlanToMarkdown(plan));
217
+ return;
218
+ }
219
+ if (subcommand === "score") {
220
+ if (rest.includes("--list-rubrics")) {
221
+ console.log(JSON.stringify(rubricPresets(), null, 2));
222
+ return;
223
+ }
224
+ // Explicit-rubric problems surface before any credential or API work.
225
+ const rubricPath = option(rest, "--rubric");
226
+ let rubric;
227
+ if (rubricPath) {
228
+ const rubricRaw = readFileSync(resolve(process.cwd(), rubricPath), "utf8");
229
+ try {
230
+ rubric = parseRubric(rubricRaw);
231
+ }
232
+ catch (error) {
233
+ throw new Error(`${rubricPath} is not a valid rubric: ${error instanceof Error ? error.message : String(error)} Expected JSON like { "scale": 5, "dimensions": [{ "name": "...", "weight": 1, "rubric": "..." }] }.`);
234
+ }
235
+ }
236
+ const callTypeOpt = option(rest, "--call-type");
237
+ if (callTypeOpt && !CALL_TYPE_IDS.includes(callTypeOpt)) {
238
+ throw new Error(`Unknown --call-type "${callTypeOpt}". One of: ${CALL_TYPE_IDS.join(", ")}.`);
239
+ }
240
+ const credential = await requireLlmCredential("score");
241
+ const transcriptPath = option(rest, "--transcript");
242
+ let transcriptText;
243
+ let title = option(rest, "--title") ?? undefined;
244
+ if (transcriptPath) {
245
+ transcriptText = normalizeTranscript(readFileSync(resolve(process.cwd(), transcriptPath), "utf8"));
246
+ }
247
+ else {
248
+ const callPath = option(rest, "--call");
249
+ if (!callPath)
250
+ throw new Error("call score requires --transcript <file> or --call <parsed.json>");
251
+ const parsed = JSON.parse(readFileSync(resolve(process.cwd(), callPath), "utf8"));
252
+ transcriptText = parsed.segments
253
+ .map((segment) => (segment.speaker ? `${segment.speaker}: ${segment.text}` : segment.text))
254
+ .join("\n");
255
+ title = title ?? parsed.title;
256
+ }
257
+ // Rubric selection: explicit --rubric wins, then --call-type, else the
258
+ // deterministic classifier picks the type-specific preset. No generic
259
+ // discovery rubric silently applied to a renewal anymore.
260
+ if (!rubric) {
261
+ const type = callTypeOpt ?? classifyCall(transcriptText).type;
262
+ rubric = rubricForCallType(type, DEFAULT_RUBRIC);
263
+ if (!rest.includes("--json")) {
264
+ const how = callTypeOpt ? `--call-type ${callTypeOpt}` : `auto-classified as ${type}`;
265
+ console.error(`Scoring with the "${rubric.name ?? "Generic"}" rubric (${how}). Override with --rubric <file> or --call-type <type>.`);
266
+ }
267
+ }
268
+ const scoreWait = startElapsedStatus((elapsed) => `Scoring against "${rubric?.name ?? "Generic"}" · ${credential.provider} · ${elapsed}`);
269
+ let scorecard;
270
+ try {
271
+ scorecard = await scoreCallLlm(transcriptText, rubric, {
272
+ ...credential,
273
+ model: option(rest, "--model") ?? undefined,
274
+ title,
275
+ });
276
+ }
277
+ finally {
278
+ scoreWait.done();
279
+ }
280
+ const outPath = option(rest, "--out");
281
+ if (outPath)
282
+ writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(scorecard, null, 2)}\n`);
283
+ if (rest.includes("--json")) {
284
+ console.log(JSON.stringify(scorecard, null, 2));
285
+ return;
286
+ }
287
+ console.log(renderScorecard(scorecard, title));
288
+ return;
289
+ }
290
+ throw new Error(`call supports: parse, classify, link, plan, score (got ${subcommand ?? "nothing"})`);
291
+ }
292
+ function renderScorecard(scorecard, title) {
293
+ const bandText = scorecard.band ? ` — ${scorecard.band.label}` : "";
294
+ const rubricLine = scorecard.rubricName ? `Rubric: ${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "";
295
+ const lines = [
296
+ `# Coaching Scorecard${title ? ` — ${title}` : ""}`,
297
+ "",
298
+ `**Overall: ${scorecard.overallScore}/${scorecard.scale}${bandText}** (model: ${scorecard.model})`,
299
+ ...(scorecard.band?.meaning ? [`> ${scorecard.band.meaning}`] : []),
300
+ ...(rubricLine ? ["", `_${rubricLine}_`] : []),
301
+ "",
302
+ "| Dimension | Score | | Coaching note |",
303
+ "| --- | --- | --- | --- |",
304
+ ];
305
+ for (const dim of scorecard.dimensions) {
306
+ const filled = Math.round((dim.score / dim.maxScore) * 5);
307
+ const bar = "█".repeat(filled) + "░".repeat(5 - filled);
308
+ lines.push(`| ${dim.name} | ${dim.score}/${dim.maxScore} | ${bar} | ${dim.coachingNote} |`);
309
+ }
310
+ if (scorecard.highlights.length) {
311
+ lines.push("", "**Highlights**");
312
+ for (const h of scorecard.highlights)
313
+ lines.push(`- ${h}`);
314
+ }
315
+ if (scorecard.missedItems.length) {
316
+ lines.push("", "**Missed**");
317
+ for (const m of scorecard.missedItems)
318
+ lines.push(`- ${m}`);
319
+ }
320
+ return lines.join("\n");
321
+ }
322
+ function buildCallPlan(parsed, deal, proposed, current, extraNextSteps) {
323
+ const findings = [];
324
+ const operations = [];
325
+ const nextStepEvidence = parsed.evidence.filter((item) => item.metadata?.insightType === "next_step");
326
+ const evidenceIds = nextStepEvidence.map((item) => item.id);
327
+ if (current.toLowerCase() !== proposed.toLowerCase()) {
328
+ findings.push({
329
+ id: `finding_${parsed.id.replace(/^call_/, "")}_${deal.id}`,
330
+ objectType: "deal",
331
+ objectId: deal.id,
332
+ ruleId: "call-next-step-not-reflected-in-crm",
333
+ type: "call_next_step_not_reflected_in_crm",
334
+ title: "Call agreed a next step the CRM does not reflect",
335
+ severity: "warning",
336
+ summary: current
337
+ ? `The call produced "${proposed}" but ${deal.name}'s next step still reads "${current}".`
338
+ : `The call produced "${proposed}" but ${deal.name} has no next step set.`,
339
+ recommendation: "Review the evidence and approve the next-step update.",
340
+ evidenceIds,
341
+ currentCrmValue: current || null,
342
+ proposedValue: proposed,
343
+ });
344
+ operations.push({
345
+ id: `op_${parsed.id.replace(/^call_/, "")}_next`,
346
+ objectType: "deal",
347
+ objectId: deal.id,
348
+ operation: "set_field",
349
+ field: "nextStep",
350
+ beforeValue: current || null,
351
+ afterValue: proposed,
352
+ reason: `Call evidence: ${nextStepEvidence[0]?.text.slice(0, 200) ?? proposed}`,
353
+ sourceRuleOrPolicy: "call_intelligence.next_step",
354
+ riskLevel: "high",
355
+ approvalRequired: true,
356
+ rollback: "Restore the previous deal next step (the before value) if the update is wrong.",
357
+ evidenceIds,
358
+ });
359
+ }
360
+ for (const [index, extra] of extraNextSteps.entries()) {
361
+ operations.push({
362
+ id: `op_${parsed.id.replace(/^call_/, "")}_task${index}`,
363
+ objectType: "deal",
364
+ objectId: deal.id,
365
+ operation: "create_task",
366
+ field: "follow_up_task",
367
+ beforeValue: null,
368
+ afterValue: extra.text.trim().slice(0, 255),
369
+ reason: `Additional commitment from the call: ${extra.evidence.slice(0, 160)}`,
370
+ sourceRuleOrPolicy: "call_intelligence.follow_up",
371
+ riskLevel: "low",
372
+ approvalRequired: true,
373
+ rollback: "Close or delete the created task.",
374
+ });
375
+ }
376
+ return {
377
+ id: `patch_plan_${parsed.id.replace(/^call_/, "")}${deal.id.slice(-4)}`,
378
+ title: `Call evidence plan${parsed.title ? ` — ${parsed.title}` : ""} → ${deal.name}`,
379
+ createdAt: new Date().toISOString(),
380
+ status: "needs_approval",
381
+ dryRun: true,
382
+ summary: `${findings.length} finding(s) and ${operations.length} proposed operation(s) from call ${parsed.id}.`,
383
+ findings,
384
+ evidence: parsed.evidence,
385
+ operations,
386
+ };
387
+ }
@@ -0,0 +1,30 @@
1
+ export type CommandAccess = "read-only" | "write-shaped";
2
+ export declare function commandAccess(name: string): CommandAccess;
3
+ export declare function capabilitiesCommand(_args: string[]): void;
4
+ export declare function robotDocsCommand(args: string[]): void;
5
+ export declare function commandHelpJsonPayload(command: string): {
6
+ ok: boolean;
7
+ command: string;
8
+ summary: string;
9
+ phase: string;
10
+ access: CommandAccess;
11
+ synopsis: string[];
12
+ detail: string | null;
13
+ options: {
14
+ flag: string;
15
+ description: string;
16
+ }[];
17
+ seeAlso: string[];
18
+ bespokeHelp: boolean;
19
+ fullReference: string;
20
+ } | null;
21
+ export { suggestCommand } from "./suggest.ts";
22
+ export declare function unknownCommandEnvelope(command: string): {
23
+ ok: false;
24
+ error: {
25
+ code: "UNKNOWN_COMMAND";
26
+ message: string;
27
+ hints: string[];
28
+ };
29
+ };
30
+ export declare function printCommandHelpJson(command: string): void;
@@ -0,0 +1,197 @@
1
+ // Machine-readable contract surfaces: `capabilities`, `robot-docs`, and the
2
+ // `--help --json` payloads.
3
+ //
4
+ // DESIGN RULE — every list in these payloads is DERIVED from an existing
5
+ // single source of truth (the HELP table in help.ts, BESPOKE_HELP,
6
+ // readPackageInfo(), the shipped SKILL.md), never hand-maintained in
7
+ // parallel. Hand-written parallel lists were the core defect of the first
8
+ // attempt at this surface (PR #156): its `mutates` list omitted `merge`, its
9
+ // `dryRunOnly` list omitted snapshot/health/suggest, and its MCP tool list
10
+ // reported 4 of 8+ tools.
11
+ import { readFileSync } from "node:fs";
12
+ import { BESPOKE_HELP, HELP } from "./help.js";
13
+ import { readPackageInfo } from "./shared.js";
14
+ import { suggestCommand } from "./suggest.js";
15
+ // Lifecycle phase → access shape. "write-shaped" means the verb participates
16
+ // in the write path (emits dry-run patch plans, applies approved operations,
17
+ // or installs schedule entries); "read-only" verbs never stage or perform a
18
+ // write. Derived from each HELP entry's `phase`, with the deviations from
19
+ // the naive phase mapping documented inline.
20
+ const PHASE_ACCESS = {
21
+ Setup: "read-only",
22
+ Detect: "read-only",
23
+ // Prevent gates writes but performs none: its one verb, `resolve`, only
24
+ // reads the snapshot and exits 0/2 — the CALLER decides whether to create.
25
+ Prevent: "read-only",
26
+ Remediate: "write-shaped",
27
+ Govern: "write-shaped",
28
+ "Govern / Verify": "write-shaped",
29
+ // Verify inspects the tamper-evident record of past applies (`audit-log`
30
+ // export/verify); it writes nothing.
31
+ Verify: "read-only",
32
+ // Continuous = `schedule`: installs crontab entries — a local write with
33
+ // ongoing side effects, and `apply --plan-id <id>` is schedulable.
34
+ Continuous: "write-shaped",
35
+ // Intelligence (`market`, `tam`) reads public pages / the CRM and writes
36
+ // only the local workspace — except `tam populate`, overridden below.
37
+ Intelligence: "read-only",
38
+ };
39
+ // Per-verb overrides for commands whose phase would misclassify them.
40
+ const ACCESS_OVERRIDES = {
41
+ // `suggest` sits on the Govern spine but is explicitly read-only: it
42
+ // derives values for requires_human_* placeholders from snapshot evidence
43
+ // and writes nothing — approval happens later in `plans approve`.
44
+ suggest: "read-only",
45
+ // `tam populate` installs recurring schedule entries (by delegating to the
46
+ // schedule layer) — the same "ongoing side effects" that make `schedule`
47
+ // write-shaped. An agent trusting this contract must not treat `tam` as
48
+ // inert.
49
+ tam: "write-shaped",
50
+ };
51
+ export function commandAccess(name) {
52
+ const override = ACCESS_OVERRIDES[name];
53
+ if (override)
54
+ return override;
55
+ const entry = HELP[name];
56
+ const access = entry ? PHASE_ACCESS[entry.phase] : undefined;
57
+ if (!access) {
58
+ // Fail loudly instead of guessing: a new phase value (or a command
59
+ // missing from HELP) must be classified deliberately, not defaulted.
60
+ throw new Error(`No access mapping for command "${name}" (phase: ${entry?.phase ?? "not in HELP"}). ` +
61
+ "Add the phase to PHASE_ACCESS or an explicit ACCESS_OVERRIDES entry in src/cli/capabilities.ts.");
62
+ }
63
+ return access;
64
+ }
65
+ // Quoted from docs/api.md ("Exit codes: `0` success · `1` error · `2`
66
+ // findings/regressions at the requested gate (`--fail-on`,
67
+ // `--fail-on-new-findings`)"), plus the gate-shaped exit-2 semantics the
68
+ // help table documents for `resolve` and `icp eval`.
69
+ const EXIT_CODES = {
70
+ "0": "success",
71
+ "1": "error",
72
+ "2": "findings/regressions at the requested gate (--fail-on, --fail-on-new-findings); resolve: match found (exists/ambiguous); icp eval: accuracy below --min-accuracy",
73
+ };
74
+ // Quoted from the Safety section of the full help reference (help.ts usage()).
75
+ const SAFETY = "Audits are read-only. Apply writes only operations you explicitly approve, " +
76
+ "and never writes requires_human_* placeholders without a --value override.";
77
+ function commandRecords() {
78
+ return Object.entries(HELP).map(([name, entry]) => ({
79
+ name,
80
+ summary: entry.summary,
81
+ phase: entry.phase,
82
+ access: commandAccess(name),
83
+ bespokeHelp: BESPOKE_HELP.includes(name),
84
+ }));
85
+ }
86
+ export function capabilitiesCommand(_args) {
87
+ const payload = {
88
+ ok: true,
89
+ tool: "fullstackgtm",
90
+ version: readPackageInfo().version,
91
+ contract: {
92
+ jsonFlag: "--json",
93
+ planFirst: true,
94
+ writePath: "write-shaped verbs emit dry-run patch plans; a CRM write happens only through `apply` on operations approved via `plans approve` (or an explicit --approve list)",
95
+ safety: SAFETY,
96
+ exitCodes: EXIT_CODES,
97
+ },
98
+ commands: commandRecords(),
99
+ help: {
100
+ perCommand: "npx fullstackgtm <command> --help [--json]",
101
+ full: "npx fullstackgtm help --full",
102
+ agentGuide: "npx fullstackgtm robot-docs",
103
+ },
104
+ mcp: {
105
+ command: "npx -y fullstackgtm-mcp",
106
+ capabilitiesTool: "fullstackgtm_capabilities",
107
+ note: "the MCP tool inventory is self-reported by the fullstackgtm_capabilities tool, derived from the server's own registration table",
108
+ },
109
+ examples: [
110
+ "npx fullstackgtm capabilities --json",
111
+ "npx fullstackgtm audit --demo --json",
112
+ "npx fullstackgtm plans list --json",
113
+ "npx fullstackgtm doctor --json",
114
+ ],
115
+ };
116
+ console.log(JSON.stringify(payload, null, 2));
117
+ }
118
+ function stripFrontmatter(markdown) {
119
+ if (!markdown.startsWith("---\n"))
120
+ return markdown;
121
+ const end = markdown.indexOf("\n---\n", 4);
122
+ if (end === -1)
123
+ return markdown;
124
+ return markdown.slice(end + "\n---\n".length);
125
+ }
126
+ export function robotDocsCommand(args) {
127
+ const topic = args.find((arg) => !arg.startsWith("-"));
128
+ if (topic && topic !== "guide") {
129
+ console.error(`Unknown robot-docs topic: ${topic}. Run \`fullstackgtm robot-docs\` for the agent guide.`);
130
+ process.exitCode = 1;
131
+ return;
132
+ }
133
+ // The package ships exactly one maintained agent guide —
134
+ // skills/fullstackgtm/SKILL.md (in the npm tarball via the `files` field).
135
+ // Print its body instead of maintaining a second hand-written guide here.
136
+ // Resolved relative to the package root, same hop as readPackageInfo().
137
+ let raw;
138
+ try {
139
+ raw = readFileSync(new URL("../../skills/fullstackgtm/SKILL.md", import.meta.url), "utf8");
140
+ }
141
+ catch {
142
+ console.error("robot-docs: the packaged agent guide (skills/fullstackgtm/SKILL.md) is missing from this install. " +
143
+ "Reinstall the package, or read it at https://github.com/fullstackgtm/core/blob/main/packages/fullstackgtm/skills/fullstackgtm/SKILL.md");
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+ console.log(stripFrontmatter(raw).trim());
148
+ }
149
+ // ── `--help --json` / `help <cmd> --json` ──────────────────────────────────
150
+ export function commandHelpJsonPayload(command) {
151
+ const entry = HELP[command];
152
+ if (!entry)
153
+ return null;
154
+ return {
155
+ ok: true,
156
+ command,
157
+ summary: entry.summary,
158
+ phase: entry.phase,
159
+ access: commandAccess(command),
160
+ synopsis: entry.synopsis,
161
+ detail: entry.detail ?? null,
162
+ options: (entry.options ?? []).map(([flag, description]) => ({ flag, description })),
163
+ seeAlso: entry.seeAlso ?? [],
164
+ // Bespoke verbs keep a richer plain-text subcommand reference of their own.
165
+ bespokeHelp: BESPOKE_HELP.includes(command),
166
+ fullReference: "npx fullstackgtm help --full",
167
+ };
168
+ }
169
+ // Nearest-command lookup lives in suggest.ts alongside the flag-typo
170
+ // handler; re-exported here so existing importers keep working.
171
+ export { suggestCommand } from "./suggest.js";
172
+ export function unknownCommandEnvelope(command) {
173
+ const suggestion = suggestCommand(command);
174
+ return {
175
+ ok: false,
176
+ error: {
177
+ code: "UNKNOWN_COMMAND",
178
+ message: `Unknown command: ${command}`,
179
+ hints: [
180
+ suggestion
181
+ ? `Did you mean: npx fullstackgtm ${suggestion}`
182
+ : "Run `npx fullstackgtm capabilities --json` to list supported commands.",
183
+ ],
184
+ },
185
+ };
186
+ }
187
+ // Print machine-readable help for one command; unknown commands get the
188
+ // structured UNKNOWN_COMMAND envelope (exit 1) instead of a text wall.
189
+ export function printCommandHelpJson(command) {
190
+ const payload = commandHelpJsonPayload(command);
191
+ if (!payload) {
192
+ console.log(JSON.stringify(unknownCommandEnvelope(command), null, 2));
193
+ process.exitCode = 1;
194
+ return;
195
+ }
196
+ console.log(JSON.stringify(payload, null, 2));
197
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `draft` — author ONE trigger-grounded opener per hot judge decision as a
3
+ * governed create_task plan. Structurally a proposal: never sends, never writes
4
+ * a CRM record. With --save the plan is staged needs_approval for plans approve
5
+ * -> apply.
6
+ */
7
+ export declare function draftCommand(args: string[]): Promise<void>;