@sonnechasser/ntrp 0.3.4 → 0.3.5

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.
@@ -113,6 +113,259 @@ var init_formatters = __esm({
113
113
  }
114
114
  });
115
115
 
116
+ // src/config/store.ts
117
+ var store_exports = {};
118
+ __export(store_exports, {
119
+ deleteConfigValue: () => deleteConfigValue,
120
+ getConfigValue: () => getConfigValue,
121
+ getConfiguredAiInboxDir: () => getConfiguredAiInboxDir,
122
+ getExportsDir: () => getExportsDir,
123
+ getKnowledgeDir: () => getKnowledgeDir,
124
+ getMemoryDir: () => getMemoryDir,
125
+ getStrategiesDir: () => getStrategiesDir,
126
+ getWinsDir: () => getWinsDir,
127
+ loadConfig: () => loadConfig,
128
+ ntrpHome: () => ntrpHome,
129
+ resetConfigCache: () => resetConfigCache,
130
+ saveConfig: () => saveConfig,
131
+ setConfigValue: () => setConfigValue
132
+ });
133
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
134
+ import { homedir } from "os";
135
+ import { join, resolve } from "path";
136
+ function ntrpHome() {
137
+ return NTRP_DIR;
138
+ }
139
+ function ensureDir() {
140
+ if (!existsSync(NTRP_DIR)) {
141
+ mkdirSync(NTRP_DIR, { recursive: true });
142
+ }
143
+ }
144
+ function loadConfig() {
145
+ if (cachedConfig) return cachedConfig;
146
+ ensureDir();
147
+ if (!existsSync(CONFIG_PATH)) {
148
+ cachedConfig = {};
149
+ return cachedConfig;
150
+ }
151
+ try {
152
+ cachedConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
153
+ } catch {
154
+ cachedConfig = {};
155
+ }
156
+ return cachedConfig;
157
+ }
158
+ function saveConfig(config) {
159
+ ensureDir();
160
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
161
+ cachedConfig = config;
162
+ }
163
+ function resetConfigCache() {
164
+ cachedConfig = null;
165
+ }
166
+ function getConfigValue(key) {
167
+ if (key === "api-key") return loadConfig()["api-key"];
168
+ if (key === "license-key") return process.env.NTRP_LICENSE_KEY ?? loadConfig()["license-key"];
169
+ const config = loadConfig();
170
+ return config[key];
171
+ }
172
+ function setConfigValue(key, value) {
173
+ const config = loadConfig();
174
+ config[key] = value;
175
+ saveConfig(config);
176
+ }
177
+ function deleteConfigValue(key) {
178
+ const config = loadConfig();
179
+ delete config[key];
180
+ saveConfig(config);
181
+ }
182
+ function getExportsDir() {
183
+ const config = loadConfig();
184
+ const dir = resolve(config["export-dir"] ?? join(NTRP_DIR, "exports"));
185
+ if (!existsSync(dir)) {
186
+ mkdirSync(dir, { recursive: true });
187
+ }
188
+ return dir;
189
+ }
190
+ function getConfiguredAiInboxDir() {
191
+ const raw = loadConfig()["ai-inbox-dir"];
192
+ return raw ? resolve(raw) : null;
193
+ }
194
+ function getStrategiesDir() {
195
+ const dir = join(NTRP_DIR, "strategies");
196
+ if (!existsSync(dir)) {
197
+ mkdirSync(dir, { recursive: true });
198
+ writeFileSync(join(dir, "README.md"), `# Strategies
199
+
200
+ This directory holds your GTM strategy files. Each file describes a strategy you're executing.
201
+
202
+ ## How to use
203
+
204
+ 1. Create a markdown file for each active strategy (e.g., \`multi-thread-q2.md\`)
205
+ 2. Describe the goal, target segment, and success criteria
206
+ 3. Reference playbook plays that support this strategy
207
+ 4. After diagnosis, check if vital signs improved in the targeted area
208
+
209
+ ## Example
210
+
211
+ \`\`\`markdown
212
+ # Multi-Thread Enterprise Deals \u2014 Q2
213
+
214
+ **Goal:** Reduce single-threaded deals from 65% to under 30%
215
+ **Segment:** Enterprise accounts > $100K
216
+ **Play:** Multi-Thread Your Deals
217
+ **Success metric:** Thread depth score > 70
218
+ \`\`\`
219
+ `);
220
+ }
221
+ return dir;
222
+ }
223
+ function getMemoryDir() {
224
+ const dir = join(NTRP_DIR, "memory");
225
+ if (!existsSync(dir)) {
226
+ mkdirSync(dir, { recursive: true });
227
+ }
228
+ return dir;
229
+ }
230
+ function getKnowledgeDir() {
231
+ const dir = join(NTRP_DIR, "knowledge");
232
+ if (!existsSync(dir)) {
233
+ mkdirSync(dir, { recursive: true });
234
+ writeFileSync(join(dir, "README.md"), `# Knowledge Packs
235
+
236
+ Drop case studies, GTM frameworks, benchmark reports, or playbooks here as
237
+ markdown, text, or PDF. NTRP ingests them with \`/knowledge add <file>\` and
238
+ references the most relevant passages during analysis \u2014 so the agent can learn
239
+ from work done outside this platform.
240
+
241
+ ## How to use
242
+
243
+ 1. Add a file: \`/knowledge add ~/Downloads/plg-benchmarks-2026.pdf\`
244
+ 2. List what's indexed: \`/knowledge list\`
245
+ 3. Ask a question \u2014 relevant passages are pulled in automatically.
246
+ `);
247
+ }
248
+ return dir;
249
+ }
250
+ function getWinsDir() {
251
+ const dir = join(NTRP_DIR, "wins");
252
+ if (!existsSync(dir)) {
253
+ mkdirSync(dir, { recursive: true });
254
+ writeFileSync(join(dir, "README.md"), `# Wins
255
+
256
+ This directory logs outcomes when a strategy or play succeeds. Each win creates a record that future diagnoses can reference.
257
+
258
+ ## How to use
259
+
260
+ 1. After executing a play, log the result here (e.g., \`2026-04-clean-pipeline.md\`)
261
+ 2. Include: what you did, what changed, before/after scores
262
+ 3. Future AI findings will reference wins to track improvement over time
263
+
264
+ ## Example
265
+
266
+ \`\`\`markdown
267
+ # Pipeline Cleanup \u2014 April 2026
268
+
269
+ **Play:** Clean Dead Pipeline
270
+ **Before:** Freshness 29/100, $3.1M stale pipeline
271
+ **After:** Freshness 72/100, removed 45 zombie deals
272
+ **Impact:** Forecast accuracy improved from 62% to 84%
273
+ \`\`\`
274
+ `);
275
+ }
276
+ return dir;
277
+ }
278
+ var NTRP_DIR, CONFIG_PATH, cachedConfig;
279
+ var init_store = __esm({
280
+ "src/config/store.ts"() {
281
+ "use strict";
282
+ NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join(homedir(), ".ntrp");
283
+ CONFIG_PATH = join(NTRP_DIR, "config.json");
284
+ cachedConfig = null;
285
+ }
286
+ });
287
+
288
+ // src/output/path-safety.ts
289
+ import { homedir as homedir2 } from "os";
290
+ import { resolve as resolve2, sep } from "path";
291
+ var NTRP_HOME;
292
+ var init_path_safety = __esm({
293
+ "src/output/path-safety.ts"() {
294
+ "use strict";
295
+ init_store();
296
+ NTRP_HOME = ntrpHome();
297
+ }
298
+ });
299
+
300
+ // src/services/exports-registry.ts
301
+ import {
302
+ appendFileSync,
303
+ copyFileSync,
304
+ cpSync,
305
+ existsSync as existsSync2,
306
+ mkdirSync as mkdirSync2,
307
+ readFileSync as readFileSync2,
308
+ readdirSync,
309
+ renameSync,
310
+ rmSync,
311
+ statSync,
312
+ writeFileSync as writeFileSync2
313
+ } from "fs";
314
+ import { basename, dirname, join as join2, resolve as resolve3, sep as sep2 } from "path";
315
+ import { randomUUID } from "crypto";
316
+ function ensureExportsLayout(root = getExportsDir()) {
317
+ mkdirSync2(root, { recursive: true });
318
+ mkdirSync2(join2(root, "latest"), { recursive: true });
319
+ for (const sub of KIND_DIRS) {
320
+ mkdirSync2(join2(root, sub), { recursive: true });
321
+ }
322
+ const readme = join2(root, "README.md");
323
+ if (!existsSync2(readme)) {
324
+ writeFileSync2(readme, ARCHIVE_README, "utf-8");
325
+ }
326
+ if (!existsSync2(join2(root, "INDEX.md"))) {
327
+ writeFileSync2(join2(root, "INDEX.md"), "# NTRP exports\n\n_No exports yet._\n", "utf-8");
328
+ }
329
+ if (!existsSync2(join2(root, "manifest.jsonl"))) {
330
+ writeFileSync2(join2(root, "manifest.jsonl"), "", "utf-8");
331
+ }
332
+ return root;
333
+ }
334
+ function getAiInboxDir() {
335
+ return getConfiguredAiInboxDir();
336
+ }
337
+ function archiveIndexPath() {
338
+ return join2(ensureExportsLayout(), "INDEX.md");
339
+ }
340
+ var KIND_DIRS, ARCHIVE_README;
341
+ var init_exports_registry = __esm({
342
+ "src/services/exports-registry.ts"() {
343
+ "use strict";
344
+ init_store();
345
+ init_path_safety();
346
+ KIND_DIRS = ["handoffs", "reports", "notes", "csv", "publish"];
347
+ ARCHIVE_README = `# NTRP exports archive
348
+
349
+ Handoffs, reports, notes, CSV receipts, and publish packages land here by kind:
350
+
351
+ - \`handoffs/\` \u2014 agent prompts (\`handoff-deck-*.md\`, \u2026)
352
+ - \`reports/\` \u2014 markdown reports
353
+ - \`notes/\` \u2014 Obsidian-style notes
354
+ - \`csv/\` \u2014 backmeup receipt folders
355
+ - \`publish/\` \u2014 repository export packages
356
+ - \`latest/\` \u2014 stable copies of the newest file per kind
357
+
358
+ \`INDEX.md\` is regenerated from \`manifest.jsonl\` on every write/move.
359
+
360
+ Point a desktop AI app at a dedicated inbox instead of this folder:
361
+
362
+ \`\`\`
363
+ /inbox set ~/Documents/Claude/ntrp-inbox
364
+ \`\`\`
365
+ `;
366
+ }
367
+ });
368
+
116
369
  // src/services/terminal-capture.ts
117
370
  function redactSecrets(line) {
118
371
  let out = line;
@@ -335,7 +588,7 @@ var init_terminal_capture = __esm({
335
588
  });
336
589
 
337
590
  // src/services/context-doc.ts
338
- import { writeFileSync } from "fs";
591
+ import { writeFileSync as writeFileSync3 } from "fs";
339
592
  function buildSessionContextDoc(file, opts = {}) {
340
593
  const id = file.id;
341
594
  const shortId = id.slice(-4);
@@ -447,6 +700,21 @@ function buildSessionContextDoc(file, opts = {}) {
447
700
  lines.push("- None yet.");
448
701
  }
449
702
  lines.push("");
703
+ lines.push("## Exports");
704
+ lines.push("");
705
+ try {
706
+ lines.push(`- Archive index: \`${archiveIndexPath()}\``);
707
+ lines.push(`- Archive root: \`${getExportsDir()}\``);
708
+ const inbox = getAiInboxDir();
709
+ if (inbox) {
710
+ lines.push(`- AI inbox: \`${inbox}\` (open \`latest-handoff.md\` or \`INDEX.md\`)`);
711
+ } else {
712
+ lines.push("- AI inbox: unset \u2014 `/inbox set <folder>` for Claude Desktop");
713
+ }
714
+ } catch {
715
+ lines.push("- Export catalog unavailable.");
716
+ }
717
+ lines.push("");
450
718
  lines.push(`## Conversation (${exchanges} exchange${exchanges === 1 ? "" : "s"})`);
451
719
  lines.push("");
452
720
  if (file.messages.length === 0) {
@@ -489,13 +757,13 @@ function writeSessionContextDoc(ctx) {
489
757
  try {
490
758
  const file = buildSessionFileSnapshot(ctx);
491
759
  const doc = buildSessionContextDoc(file, { snapshot: ctx.snapshot.computeResult });
492
- writeFileSync(contextDocPathForSession(ctx.sessionId), doc);
760
+ writeFileSync3(contextDocPathForSession(ctx.sessionId), doc);
493
761
  } catch {
494
762
  }
495
763
  }
496
764
  function writeContextDocForSessionFile(file, opts = {}) {
497
765
  try {
498
- writeFileSync(contextDocPathForSession(file.id), buildSessionContextDoc(file, opts));
766
+ writeFileSync3(contextDocPathForSession(file.id), buildSessionContextDoc(file, opts));
499
767
  } catch {
500
768
  }
501
769
  }
@@ -505,18 +773,20 @@ var init_context_doc = __esm({
505
773
  "use strict";
506
774
  init_context2();
507
775
  init_formatters();
776
+ init_store();
777
+ init_exports_registry();
508
778
  init_terminal_capture();
509
779
  AGENT_EXCERPT_CHARS = 400;
510
780
  }
511
781
  });
512
782
 
513
783
  // src/services/transcript.ts
514
- import { existsSync, readFileSync, writeFileSync as writeFileSync2, rmSync } from "fs";
515
- import { join } from "path";
784
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
785
+ import { join as join3 } from "path";
516
786
  function rebindSessionTranscript(ctx) {
517
787
  if (!state || state.sessionId === ctx.sessionId) return;
518
- const priorJson = join(getSessionsDir(), `${state.sessionId}.json`);
519
- if (existsSync(priorJson)) {
788
+ const priorJson = join3(getSessionsDir(), `${state.sessionId}.json`);
789
+ if (existsSync3(priorJson)) {
520
790
  finalizeCurrentFile("switched session");
521
791
  } else {
522
792
  discardSessionTranscript(state.sessionId);
@@ -530,16 +800,16 @@ function discardSessionTranscript(sessionId) {
530
800
  clearFlushTimer();
531
801
  }
532
802
  try {
533
- rmSync(transcriptPathForSession(sessionId), { force: true });
803
+ rmSync2(transcriptPathForSession(sessionId), { force: true });
534
804
  } catch {
535
805
  }
536
806
  }
537
807
  function createState(sessionId) {
538
808
  const filePath = transcriptPathForSession(sessionId);
539
809
  let base = "";
540
- if (existsSync(filePath)) {
810
+ if (existsSync3(filePath)) {
541
811
  try {
542
- base = readFileSync(filePath, "utf-8").trimEnd() + "\n";
812
+ base = readFileSync3(filePath, "utf-8").trimEnd() + "\n";
543
813
  } catch {
544
814
  base = "";
545
815
  }
@@ -599,7 +869,7 @@ function flushNow(closedNote) {
599
869
  s.lastFlushMs = Date.now();
600
870
  try {
601
871
  getSessionsDir();
602
- writeFileSync2(s.filePath, render(s, closedNote));
872
+ writeFileSync4(s.filePath, render(s, closedNote));
603
873
  } catch {
604
874
  }
605
875
  }
@@ -638,12 +908,12 @@ __export(connection_exports, {
638
908
  run: () => run,
639
909
  setActiveDbPath: () => setActiveDbPath
640
910
  });
641
- import { mkdirSync, existsSync as existsSync2, rmSync as rmSync2 } from "fs";
642
- import { dirname, join as join2, resolve } from "path";
643
- function ensureDir() {
644
- const dir = dirname(activeDbPath);
645
- if (!existsSync2(dir)) {
646
- mkdirSync(dir, { recursive: true });
911
+ import { mkdirSync as mkdirSync3, existsSync as existsSync4, rmSync as rmSync3 } from "fs";
912
+ import { dirname as dirname2, join as join4, resolve as resolve4 } from "path";
913
+ function ensureDir2() {
914
+ const dir = dirname2(activeDbPath);
915
+ if (!existsSync4(dir)) {
916
+ mkdirSync3(dir, { recursive: true });
647
917
  }
648
918
  }
649
919
  function getActiveDbPath() {
@@ -651,7 +921,7 @@ function getActiveDbPath() {
651
921
  }
652
922
  async function setActiveDbPath(path) {
653
923
  if (DB_PATH_PINNED) return;
654
- const resolved = resolve(path);
924
+ const resolved = resolve4(path);
655
925
  if (resolved === activeDbPath) return;
656
926
  await discardConnection();
657
927
  activeDbPath = resolved;
@@ -671,7 +941,7 @@ async function getConnection() {
671
941
  }
672
942
  await discardConnection();
673
943
  }
674
- ensureDir();
944
+ ensureDir2();
675
945
  const duckdb = await loadDuckDB();
676
946
  db = new duckdb.Database(activeDbPath);
677
947
  conn = new duckdb.Connection(db);
@@ -689,20 +959,20 @@ function isClosedConnectionError(err) {
689
959
  function closeConnection(c) {
690
960
  const close2 = c.close;
691
961
  if (typeof close2 !== "function") return Promise.resolve();
692
- return new Promise((resolve8) => {
962
+ return new Promise((resolve10) => {
693
963
  try {
694
- close2.call(c, () => resolve8());
964
+ close2.call(c, () => resolve10());
695
965
  } catch {
696
- resolve8();
966
+ resolve10();
697
967
  }
698
968
  });
699
969
  }
700
970
  function isConnectionAlive(c) {
701
- return new Promise((resolve8) => {
971
+ return new Promise((resolve10) => {
702
972
  try {
703
- c.all("SELECT 1", (err) => resolve8(!err));
973
+ c.all("SELECT 1", (err) => resolve10(!err));
704
974
  } catch {
705
- resolve8(false);
975
+ resolve10(false);
706
976
  }
707
977
  });
708
978
  }
@@ -716,8 +986,8 @@ async function discardConnection() {
716
986
  await closeConnection(currentConn).catch(() => void 0);
717
987
  }
718
988
  if (currentDb) {
719
- await new Promise((resolve8) => {
720
- currentDb.close(() => resolve8());
989
+ await new Promise((resolve10) => {
990
+ currentDb.close(() => resolve10());
721
991
  }).catch(() => void 0);
722
992
  }
723
993
  }
@@ -732,10 +1002,10 @@ async function withReconnect(op) {
732
1002
  }
733
1003
  async function execAllOnce(sql, params) {
734
1004
  const c = await getConnection();
735
- return new Promise((resolve8, reject) => {
1005
+ return new Promise((resolve10, reject) => {
736
1006
  const cb = (err, rows) => {
737
1007
  if (err) reject(err);
738
- else resolve8(rows ?? []);
1008
+ else resolve10(rows ?? []);
739
1009
  };
740
1010
  if (params.length > 0) {
741
1011
  const stmt = c.prepare(sql);
@@ -750,18 +1020,18 @@ async function execAllOnce(sql, params) {
750
1020
  }
751
1021
  async function runOnce(sql, params = []) {
752
1022
  const c = await getConnection();
753
- return new Promise((resolve8, reject) => {
1023
+ return new Promise((resolve10, reject) => {
754
1024
  if (params.length > 0) {
755
1025
  const stmt = c.prepare(sql);
756
1026
  stmt.run(...params, (err) => {
757
1027
  stmt.finalize();
758
1028
  if (err) reject(err);
759
- else resolve8();
1029
+ else resolve10();
760
1030
  });
761
1031
  } else {
762
1032
  c.run(sql, (err) => {
763
1033
  if (err) reject(err);
764
- else resolve8();
1034
+ else resolve10();
765
1035
  });
766
1036
  }
767
1037
  });
@@ -781,15 +1051,15 @@ async function close() {
781
1051
  async function recreateDatabaseFile() {
782
1052
  await discardConnection();
783
1053
  for (const path of [activeDbPath, `${activeDbPath}.wal`]) {
784
- rmSync2(path, { force: true });
1054
+ rmSync3(path, { force: true });
785
1055
  }
786
1056
  }
787
- var NTRP_DIR, DEFAULT_DB_PATH, DB_PATH_PINNED, activeDbPath, db, conn, duckdbModule, connectionGeneration, lastHealthCheckMs, HEALTH_CHECK_INTERVAL_MS;
1057
+ var NTRP_DIR2, DEFAULT_DB_PATH, DB_PATH_PINNED, activeDbPath, db, conn, duckdbModule, connectionGeneration, lastHealthCheckMs, HEALTH_CHECK_INTERVAL_MS;
788
1058
  var init_connection = __esm({
789
1059
  "src/db/connection.ts"() {
790
1060
  "use strict";
791
- NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join2(process.env.HOME ?? "", ".ntrp");
792
- DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ? resolve(process.env.NTRP_DB_PATH) : join2(NTRP_DIR, "ntrp.duckdb");
1061
+ NTRP_DIR2 = process.env.NTRP_HOME ? resolve4(process.env.NTRP_HOME) : join4(process.env.HOME ?? "", ".ntrp");
1062
+ DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ? resolve4(process.env.NTRP_DB_PATH) : join4(NTRP_DIR2, "ntrp.duckdb");
793
1063
  DB_PATH_PINNED = !!process.env.NTRP_DB_PATH;
794
1064
  activeDbPath = DEFAULT_DB_PATH;
795
1065
  db = null;
@@ -859,9 +1129,9 @@ __export(queries_exports, {
859
1129
  upsertStrategy: () => upsertStrategy,
860
1130
  uuid: () => uuid
861
1131
  });
862
- import { randomUUID } from "crypto";
1132
+ import { randomUUID as randomUUID2 } from "crypto";
863
1133
  function uuid() {
864
- return randomUUID();
1134
+ return randomUUID2();
865
1135
  }
866
1136
  function now() {
867
1137
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -1907,216 +2177,49 @@ CREATE INDEX IF NOT EXISTS idx_health_readings_batch ON health_readings(upload_b
1907
2177
  }
1908
2178
  });
1909
2179
 
1910
- // src/config/store.ts
1911
- var store_exports = {};
1912
- __export(store_exports, {
1913
- deleteConfigValue: () => deleteConfigValue,
1914
- getConfigValue: () => getConfigValue,
1915
- getExportsDir: () => getExportsDir,
1916
- getKnowledgeDir: () => getKnowledgeDir,
1917
- getMemoryDir: () => getMemoryDir,
1918
- getStrategiesDir: () => getStrategiesDir,
1919
- getWinsDir: () => getWinsDir,
1920
- loadConfig: () => loadConfig,
1921
- ntrpHome: () => ntrpHome,
1922
- resetConfigCache: () => resetConfigCache,
1923
- saveConfig: () => saveConfig,
1924
- setConfigValue: () => setConfigValue
1925
- });
1926
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1927
- import { homedir } from "os";
1928
- import { join as join3, resolve as resolve2 } from "path";
1929
- function ntrpHome() {
1930
- return NTRP_DIR2;
2180
+ // src/config/install.ts
2181
+ import { randomUUID as randomUUID3 } from "crypto";
2182
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
2183
+ import { join as join5 } from "path";
2184
+ function installPath() {
2185
+ return join5(ntrpHome(), "install.json");
1931
2186
  }
1932
- function ensureDir2() {
1933
- if (!existsSync3(NTRP_DIR2)) {
1934
- mkdirSync2(NTRP_DIR2, { recursive: true });
2187
+ function ensureDir3() {
2188
+ const dir = ntrpHome();
2189
+ if (!existsSync5(dir)) {
2190
+ mkdirSync4(dir, { recursive: true });
1935
2191
  }
1936
2192
  }
1937
- function loadConfig() {
1938
- if (cachedConfig) return cachedConfig;
1939
- ensureDir2();
1940
- if (!existsSync3(CONFIG_PATH)) {
1941
- cachedConfig = {};
1942
- return cachedConfig;
1943
- }
1944
- try {
1945
- cachedConfig = JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
1946
- } catch {
1947
- cachedConfig = {};
1948
- }
1949
- return cachedConfig;
2193
+ function isValidInstall(value) {
2194
+ if (!value || typeof value !== "object") return false;
2195
+ const r = value;
2196
+ return r.schema_version === 1 && typeof r.install_id === "string" && r.install_id.length > 0 && typeof r.created_at === "string";
1950
2197
  }
1951
- function saveConfig(config) {
1952
- ensureDir2();
1953
- writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
1954
- cachedConfig = config;
2198
+ function ensureInstall() {
2199
+ if (cachedInstall) return cachedInstall;
2200
+ const path = installPath();
2201
+ if (existsSync5(path)) {
2202
+ try {
2203
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
2204
+ if (isValidInstall(parsed)) {
2205
+ cachedInstall = parsed;
2206
+ return parsed;
2207
+ }
2208
+ } catch {
2209
+ }
2210
+ }
2211
+ const record = {
2212
+ schema_version: 1,
2213
+ install_id: randomUUID3(),
2214
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2215
+ };
2216
+ ensureDir3();
2217
+ writeFileSync5(path, JSON.stringify(record, null, 2) + "\n");
2218
+ cachedInstall = record;
2219
+ return record;
1955
2220
  }
1956
- function resetConfigCache() {
1957
- cachedConfig = null;
1958
- }
1959
- function getConfigValue(key) {
1960
- if (key === "api-key") return loadConfig()["api-key"];
1961
- if (key === "license-key") return process.env.NTRP_LICENSE_KEY ?? loadConfig()["license-key"];
1962
- const config = loadConfig();
1963
- return config[key];
1964
- }
1965
- function setConfigValue(key, value) {
1966
- const config = loadConfig();
1967
- config[key] = value;
1968
- saveConfig(config);
1969
- }
1970
- function deleteConfigValue(key) {
1971
- const config = loadConfig();
1972
- delete config[key];
1973
- saveConfig(config);
1974
- }
1975
- function getExportsDir() {
1976
- const config = loadConfig();
1977
- const dir = resolve2(config["export-dir"] ?? join3(NTRP_DIR2, "exports"));
1978
- if (!existsSync3(dir)) {
1979
- mkdirSync2(dir, { recursive: true });
1980
- }
1981
- return dir;
1982
- }
1983
- function getStrategiesDir() {
1984
- const dir = join3(NTRP_DIR2, "strategies");
1985
- if (!existsSync3(dir)) {
1986
- mkdirSync2(dir, { recursive: true });
1987
- writeFileSync3(join3(dir, "README.md"), `# Strategies
1988
-
1989
- This directory holds your GTM strategy files. Each file describes a strategy you're executing.
1990
-
1991
- ## How to use
1992
-
1993
- 1. Create a markdown file for each active strategy (e.g., \`multi-thread-q2.md\`)
1994
- 2. Describe the goal, target segment, and success criteria
1995
- 3. Reference playbook plays that support this strategy
1996
- 4. After diagnosis, check if vital signs improved in the targeted area
1997
-
1998
- ## Example
1999
-
2000
- \`\`\`markdown
2001
- # Multi-Thread Enterprise Deals \u2014 Q2
2002
-
2003
- **Goal:** Reduce single-threaded deals from 65% to under 30%
2004
- **Segment:** Enterprise accounts > $100K
2005
- **Play:** Multi-Thread Your Deals
2006
- **Success metric:** Thread depth score > 70
2007
- \`\`\`
2008
- `);
2009
- }
2010
- return dir;
2011
- }
2012
- function getMemoryDir() {
2013
- const dir = join3(NTRP_DIR2, "memory");
2014
- if (!existsSync3(dir)) {
2015
- mkdirSync2(dir, { recursive: true });
2016
- }
2017
- return dir;
2018
- }
2019
- function getKnowledgeDir() {
2020
- const dir = join3(NTRP_DIR2, "knowledge");
2021
- if (!existsSync3(dir)) {
2022
- mkdirSync2(dir, { recursive: true });
2023
- writeFileSync3(join3(dir, "README.md"), `# Knowledge Packs
2024
-
2025
- Drop case studies, GTM frameworks, benchmark reports, or playbooks here as
2026
- markdown, text, or PDF. NTRP ingests them with \`/knowledge add <file>\` and
2027
- references the most relevant passages during analysis \u2014 so the agent can learn
2028
- from work done outside this platform.
2029
-
2030
- ## How to use
2031
-
2032
- 1. Add a file: \`/knowledge add ~/Downloads/plg-benchmarks-2026.pdf\`
2033
- 2. List what's indexed: \`/knowledge list\`
2034
- 3. Ask a question \u2014 relevant passages are pulled in automatically.
2035
- `);
2036
- }
2037
- return dir;
2038
- }
2039
- function getWinsDir() {
2040
- const dir = join3(NTRP_DIR2, "wins");
2041
- if (!existsSync3(dir)) {
2042
- mkdirSync2(dir, { recursive: true });
2043
- writeFileSync3(join3(dir, "README.md"), `# Wins
2044
-
2045
- This directory logs outcomes when a strategy or play succeeds. Each win creates a record that future diagnoses can reference.
2046
-
2047
- ## How to use
2048
-
2049
- 1. After executing a play, log the result here (e.g., \`2026-04-clean-pipeline.md\`)
2050
- 2. Include: what you did, what changed, before/after scores
2051
- 3. Future AI findings will reference wins to track improvement over time
2052
-
2053
- ## Example
2054
-
2055
- \`\`\`markdown
2056
- # Pipeline Cleanup \u2014 April 2026
2057
-
2058
- **Play:** Clean Dead Pipeline
2059
- **Before:** Freshness 29/100, $3.1M stale pipeline
2060
- **After:** Freshness 72/100, removed 45 zombie deals
2061
- **Impact:** Forecast accuracy improved from 62% to 84%
2062
- \`\`\`
2063
- `);
2064
- }
2065
- return dir;
2066
- }
2067
- var NTRP_DIR2, CONFIG_PATH, cachedConfig;
2068
- var init_store = __esm({
2069
- "src/config/store.ts"() {
2070
- "use strict";
2071
- NTRP_DIR2 = process.env.NTRP_HOME ? resolve2(process.env.NTRP_HOME) : join3(homedir(), ".ntrp");
2072
- CONFIG_PATH = join3(NTRP_DIR2, "config.json");
2073
- cachedConfig = null;
2074
- }
2075
- });
2076
-
2077
- // src/config/install.ts
2078
- import { randomUUID as randomUUID2 } from "crypto";
2079
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
2080
- import { join as join4 } from "path";
2081
- function installPath() {
2082
- return join4(ntrpHome(), "install.json");
2083
- }
2084
- function ensureDir3() {
2085
- const dir = ntrpHome();
2086
- if (!existsSync4(dir)) {
2087
- mkdirSync3(dir, { recursive: true });
2088
- }
2089
- }
2090
- function isValidInstall(value) {
2091
- if (!value || typeof value !== "object") return false;
2092
- const r = value;
2093
- return r.schema_version === 1 && typeof r.install_id === "string" && r.install_id.length > 0 && typeof r.created_at === "string";
2094
- }
2095
- function ensureInstall() {
2096
- if (cachedInstall) return cachedInstall;
2097
- const path = installPath();
2098
- if (existsSync4(path)) {
2099
- try {
2100
- const parsed = JSON.parse(readFileSync3(path, "utf-8"));
2101
- if (isValidInstall(parsed)) {
2102
- cachedInstall = parsed;
2103
- return parsed;
2104
- }
2105
- } catch {
2106
- }
2107
- }
2108
- const record = {
2109
- schema_version: 1,
2110
- install_id: randomUUID2(),
2111
- created_at: (/* @__PURE__ */ new Date()).toISOString()
2112
- };
2113
- ensureDir3();
2114
- writeFileSync4(path, JSON.stringify(record, null, 2) + "\n");
2115
- cachedInstall = record;
2116
- return record;
2117
- }
2118
- function getInstallId() {
2119
- return ensureInstall().install_id;
2221
+ function getInstallId() {
2222
+ return ensureInstall().install_id;
2120
2223
  }
2121
2224
  var cachedInstall;
2122
2225
  var init_install = __esm({
@@ -2128,16 +2231,16 @@ var init_install = __esm({
2128
2231
  });
2129
2232
 
2130
2233
  // src/config/progress-migrate.ts
2131
- import { existsSync as existsSync5, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync5 } from "fs";
2132
- import { join as join5 } from "path";
2234
+ import { existsSync as existsSync6, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
2235
+ import { join as join6 } from "path";
2133
2236
  function legacyStatePath() {
2134
- return join5(ntrpHome(), "state.json");
2237
+ return join6(ntrpHome(), "state.json");
2135
2238
  }
2136
2239
  function legacyStateBackupPath() {
2137
- return join5(ntrpHome(), "state.json.bak");
2240
+ return join6(ntrpHome(), "state.json.bak");
2138
2241
  }
2139
2242
  function progressPath() {
2140
- return join5(ntrpHome(), "progress.json");
2243
+ return join6(ntrpHome(), "progress.json");
2141
2244
  }
2142
2245
  function isValidLegacyState(value) {
2143
2246
  if (!value || typeof value !== "object") return false;
@@ -2145,11 +2248,11 @@ function isValidLegacyState(value) {
2145
2248
  return s.schema_version === 1 && typeof s.total_minutes_saved === "number" && Array.isArray(s.credits) && Array.isArray(s.milestones_unlocked);
2146
2249
  }
2147
2250
  function migrateLegacyStateIfNeeded(installId) {
2148
- if (existsSync5(progressPath())) return null;
2251
+ if (existsSync6(progressPath())) return null;
2149
2252
  const legacyPath = legacyStatePath();
2150
- if (!existsSync5(legacyPath)) return null;
2253
+ if (!existsSync6(legacyPath)) return null;
2151
2254
  try {
2152
- const parsed = JSON.parse(readFileSync4(legacyPath, "utf-8"));
2255
+ const parsed = JSON.parse(readFileSync5(legacyPath, "utf-8"));
2153
2256
  if (!isValidLegacyState(parsed)) return null;
2154
2257
  const { schema_version: _v, ...rest } = parsed;
2155
2258
  const progress = {
@@ -2157,9 +2260,9 @@ function migrateLegacyStateIfNeeded(installId) {
2157
2260
  schema_version: 2,
2158
2261
  install_id: installId
2159
2262
  };
2160
- writeFileSync5(progressPath(), JSON.stringify(progress, null, 2) + "\n");
2263
+ writeFileSync6(progressPath(), JSON.stringify(progress, null, 2) + "\n");
2161
2264
  try {
2162
- renameSync(legacyPath, legacyStateBackupPath());
2265
+ renameSync2(legacyPath, legacyStateBackupPath());
2163
2266
  } catch {
2164
2267
  }
2165
2268
  return progress;
@@ -2264,15 +2367,15 @@ var init_usage_backfill = __esm({
2264
2367
  });
2265
2368
 
2266
2369
  // src/config/progress.ts
2267
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
2268
- import { join as join6 } from "path";
2370
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
2371
+ import { join as join7 } from "path";
2269
2372
  function progressPath2() {
2270
- return join6(ntrpHome(), "progress.json");
2373
+ return join7(ntrpHome(), "progress.json");
2271
2374
  }
2272
2375
  function ensureDir4() {
2273
2376
  const dir = ntrpHome();
2274
- if (!existsSync6(dir)) {
2275
- mkdirSync4(dir, { recursive: true });
2377
+ if (!existsSync7(dir)) {
2378
+ mkdirSync5(dir, { recursive: true });
2276
2379
  }
2277
2380
  }
2278
2381
  function emptyProgress(installId) {
@@ -2302,9 +2405,9 @@ function reconcileInstallId(state2) {
2302
2405
  }
2303
2406
  function readProgressFile() {
2304
2407
  const path = progressPath2();
2305
- if (!existsSync6(path)) return { state: null, changed: false };
2408
+ if (!existsSync7(path)) return { state: null, changed: false };
2306
2409
  try {
2307
- const parsed = JSON.parse(readFileSync5(path, "utf-8"));
2410
+ const parsed = JSON.parse(readFileSync6(path, "utf-8"));
2308
2411
  if (!isValidProgress(parsed)) return { state: null, changed: false };
2309
2412
  return reconcileInstallId(parsed);
2310
2413
  } catch {
@@ -2345,7 +2448,7 @@ function saveProgress(state2) {
2345
2448
  schema_version: 2,
2346
2449
  install_id: getInstallId()
2347
2450
  };
2348
- writeFileSync6(progressPath2(), JSON.stringify(next, null, 2) + "\n");
2451
+ writeFileSync7(progressPath2(), JSON.stringify(next, null, 2) + "\n");
2349
2452
  }
2350
2453
  function appendCredit(state2, credit) {
2351
2454
  const credits = [...state2.credits, credit];
@@ -3111,20 +3214,20 @@ var init_time_bank = __esm({
3111
3214
  });
3112
3215
 
3113
3216
  // src/ai/llm/providers.ts
3114
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
3115
- import { join as join7 } from "path";
3217
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
3218
+ import { join as join8 } from "path";
3116
3219
  function providersPath() {
3117
- return join7(ntrpHome(), "providers.json");
3220
+ return join8(ntrpHome(), "providers.json");
3118
3221
  }
3119
3222
  function loadCustomProviders() {
3120
3223
  if (cachedEntries) return cachedEntries;
3121
3224
  const path = providersPath();
3122
- if (!existsSync7(path)) {
3225
+ if (!existsSync8(path)) {
3123
3226
  cachedEntries = [];
3124
3227
  return cachedEntries;
3125
3228
  }
3126
3229
  try {
3127
- const parsed = JSON.parse(readFileSync6(path, "utf-8"));
3230
+ const parsed = JSON.parse(readFileSync7(path, "utf-8"));
3128
3231
  cachedEntries = Array.isArray(parsed.providers) ? parsed.providers : [];
3129
3232
  } catch {
3130
3233
  cachedEntries = [];
@@ -3885,20 +3988,20 @@ var init_openai_compat = __esm({
3885
3988
  });
3886
3989
 
3887
3990
  // src/ai/llm/models-cache.ts
3888
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
3889
- import { join as join8 } from "path";
3991
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
3992
+ import { join as join9 } from "path";
3890
3993
  function cachePath() {
3891
- return join8(ntrpHome(), "models.json");
3994
+ return join9(ntrpHome(), "models.json");
3892
3995
  }
3893
3996
  function loadFile() {
3894
3997
  if (cached) return cached;
3895
3998
  const path = cachePath();
3896
- if (!existsSync8(path)) {
3999
+ if (!existsSync9(path)) {
3897
4000
  cached = { version: 1, providers: {} };
3898
4001
  return cached;
3899
4002
  }
3900
4003
  try {
3901
- const parsed = JSON.parse(readFileSync7(path, "utf-8"));
4004
+ const parsed = JSON.parse(readFileSync8(path, "utf-8"));
3902
4005
  cached = { version: 1, providers: parsed.providers ?? {} };
3903
4006
  } catch {
3904
4007
  cached = { version: 1, providers: {} };
@@ -3906,7 +4009,7 @@ function loadFile() {
3906
4009
  return cached;
3907
4010
  }
3908
4011
  function saveFile(file) {
3909
- writeFileSync8(cachePath(), JSON.stringify(file, null, 2) + "\n");
4012
+ writeFileSync9(cachePath(), JSON.stringify(file, null, 2) + "\n");
3910
4013
  cached = file;
3911
4014
  }
3912
4015
  function getProviderModels(provider) {
@@ -4067,10 +4170,10 @@ var init_catalog = __esm({
4067
4170
  });
4068
4171
 
4069
4172
  // src/ai/llm/http.ts
4070
- import { readFileSync as readFileSync8 } from "fs";
4173
+ import { readFileSync as readFileSync9 } from "fs";
4071
4174
  function fixtureResponse(url, headers) {
4072
4175
  try {
4073
- const raw = readFileSync8(process.env.NTRP_LLM_HTTP_FIXTURE, "utf-8");
4176
+ const raw = readFileSync9(process.env.NTRP_LLM_HTTP_FIXTURE, "utf-8");
4074
4177
  const entries = JSON.parse(raw);
4075
4178
  const headerValues = Object.values(headers).join(" ");
4076
4179
  for (const entry of entries) {
@@ -5038,24 +5141,24 @@ var init_errors2 = __esm({
5038
5141
 
5039
5142
  // src/strategies/readers.ts
5040
5143
  import { createHash } from "crypto";
5041
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
5042
- import { extname, resolve as resolve3 } from "path";
5144
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
5145
+ import { extname, resolve as resolve5 } from "path";
5043
5146
  import { parse as parseYaml } from "yaml";
5044
5147
  import { PDFParse } from "pdf-parse";
5045
5148
  async function readStrategyFile(pathOrDash) {
5046
5149
  if (pathOrDash === "-") {
5047
- const text2 = readFileSync9(0, "utf-8");
5150
+ const text2 = readFileSync10(0, "utf-8");
5048
5151
  return createDocument("stdin", null, text2, {});
5049
5152
  }
5050
- const sourcePath = resolve3(pathOrDash);
5051
- if (!existsSync9(sourcePath)) {
5153
+ const sourcePath = resolve5(pathOrDash);
5154
+ if (!existsSync10(sourcePath)) {
5052
5155
  throw new NtrpError("strategy_file_not_found", `Strategy file not found: ${pathOrDash}`, 2 /* Usage */);
5053
5156
  }
5054
5157
  const ext = extname(sourcePath).toLowerCase();
5055
5158
  if (ext === ".pdf") {
5056
5159
  return readPdf(sourcePath);
5057
5160
  }
5058
- const text = readFileSync9(sourcePath, "utf-8");
5161
+ const text = readFileSync10(sourcePath, "utf-8");
5059
5162
  if (ext === ".yaml" || ext === ".yml") {
5060
5163
  const structured = parseStructuredYaml(text);
5061
5164
  return createDocument("yaml", sourcePath, text, structured);
@@ -5070,7 +5173,7 @@ function readStrategyText(text) {
5070
5173
  return createDocument("text", null, text, {});
5071
5174
  }
5072
5175
  async function readPdf(sourcePath) {
5073
- const data = readFileSync9(sourcePath);
5176
+ const data = readFileSync10(sourcePath);
5074
5177
  const parser = new PDFParse({ data });
5075
5178
  try {
5076
5179
  const result = await parser.getText();
@@ -5117,17 +5220,17 @@ var init_readers = __esm({
5117
5220
  });
5118
5221
 
5119
5222
  // src/memory/knowledge.ts
5120
- import { existsSync as existsSync10, readFileSync as readFileSync10, appendFileSync, readdirSync } from "fs";
5121
- import { join as join9 } from "path";
5122
- import { randomUUID as randomUUID3 } from "crypto";
5223
+ import { existsSync as existsSync11, readFileSync as readFileSync11, appendFileSync as appendFileSync2, readdirSync as readdirSync2 } from "fs";
5224
+ import { join as join10 } from "path";
5225
+ import { randomUUID as randomUUID4 } from "crypto";
5123
5226
  function knowledgePath() {
5124
- return join9(getMemoryDir(), KNOWLEDGE_FILE);
5227
+ return join10(getMemoryDir(), KNOWLEDGE_FILE);
5125
5228
  }
5126
5229
  function loadKnowledgeChunks() {
5127
5230
  const path = knowledgePath();
5128
- if (!existsSync10(path)) return [];
5231
+ if (!existsSync11(path)) return [];
5129
5232
  const out = [];
5130
- for (const line of readFileSync10(path, "utf-8").split("\n")) {
5233
+ for (const line of readFileSync11(path, "utf-8").split("\n")) {
5131
5234
  const trimmed = line.trim();
5132
5235
  if (!trimmed) continue;
5133
5236
  try {
@@ -5160,16 +5263,16 @@ __export(playbook_exports, {
5160
5263
  getPlaysForVitalSign: () => getPlaysForVitalSign,
5161
5264
  matchTriggeredPlays: () => matchTriggeredPlays
5162
5265
  });
5163
- import { existsSync as existsSync11, readFileSync as readFileSync11, appendFileSync as appendFileSync2 } from "fs";
5164
- import { join as join10 } from "path";
5266
+ import { existsSync as existsSync12, readFileSync as readFileSync12, appendFileSync as appendFileSync3 } from "fs";
5267
+ import { join as join11 } from "path";
5165
5268
  function playsPath() {
5166
- return join10(getMemoryDir(), PLAYS_FILE);
5269
+ return join11(getMemoryDir(), PLAYS_FILE);
5167
5270
  }
5168
5271
  function getCustomPlays() {
5169
5272
  const path = playsPath();
5170
- if (!existsSync11(path)) return [];
5273
+ if (!existsSync12(path)) return [];
5171
5274
  const out = [];
5172
- for (const line of readFileSync11(path, "utf-8").split("\n")) {
5275
+ for (const line of readFileSync12(path, "utf-8").split("\n")) {
5173
5276
  const trimmed = line.trim();
5174
5277
  if (!trimmed) continue;
5175
5278
  try {
@@ -5201,7 +5304,7 @@ function addCustomPlay(input) {
5201
5304
  source: "learned"
5202
5305
  };
5203
5306
  try {
5204
- appendFileSync2(playsPath(), JSON.stringify(play) + "\n");
5307
+ appendFileSync3(playsPath(), JSON.stringify(play) + "\n");
5205
5308
  } catch {
5206
5309
  }
5207
5310
  return play;
@@ -5591,15 +5694,15 @@ JSON SHAPE:
5591
5694
  });
5592
5695
 
5593
5696
  // src/strategies/library.ts
5594
- import { writeFileSync as writeFileSync9 } from "fs";
5595
- import { join as join11 } from "path";
5697
+ import { writeFileSync as writeFileSync10 } from "fs";
5698
+ import { join as join12 } from "path";
5596
5699
  import { stringify as stringifyYaml } from "yaml";
5597
5700
  function strategyLibraryPath(slug) {
5598
- return join11(getStrategiesDir(), `${slug}.md`);
5701
+ return join12(getStrategiesDir(), `${slug}.md`);
5599
5702
  }
5600
5703
  function writeStrategyMarkdown(strategy) {
5601
5704
  const path = strategyLibraryPath(strategy.slug);
5602
- writeFileSync9(path, renderStrategyMarkdown(strategy), "utf-8");
5705
+ writeFileSync10(path, renderStrategyMarkdown(strategy), "utf-8");
5603
5706
  return path;
5604
5707
  }
5605
5708
  function renderStrategyMarkdown(strategy) {
@@ -5723,12 +5826,12 @@ var init_library = __esm({
5723
5826
  });
5724
5827
 
5725
5828
  // src/strategies/connectors.ts
5726
- import { readdirSync as readdirSync2, statSync } from "fs";
5727
- import { homedir as homedir2 } from "os";
5728
- import { basename, extname as extname2, join as join12, relative, resolve as resolve4, sep } from "path";
5829
+ import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
5830
+ import { homedir as homedir3 } from "os";
5831
+ import { basename as basename2, extname as extname2, join as join13, relative, resolve as resolve6, sep as sep3 } from "path";
5729
5832
  function createLocalFolderConnector(options) {
5730
- const rootPath = resolveUserPath(options.rootPath);
5731
- const name = options.name ?? (basename(rootPath) || "local");
5833
+ const rootPath = resolveUserPath2(options.rootPath);
5834
+ const name = options.name ?? (basename2(rootPath) || "local");
5732
5835
  const includePatterns = normalizePatterns(options.includePatterns);
5733
5836
  const excludePatterns = normalizePatterns(options.excludePatterns);
5734
5837
  const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
@@ -5767,8 +5870,8 @@ function createLocalFolderConnector(options) {
5767
5870
  };
5768
5871
  }
5769
5872
  function walkLocalFolder(rootPath, currentPath, refs, opts) {
5770
- for (const entry of readdirSync2(currentPath, { withFileTypes: true })) {
5771
- const absolutePath = join12(currentPath, entry.name);
5873
+ for (const entry of readdirSync3(currentPath, { withFileTypes: true })) {
5874
+ const absolutePath = join13(currentPath, entry.name);
5772
5875
  const relativePath = normalizePath(relative(rootPath, absolutePath));
5773
5876
  if (entry.isDirectory()) {
5774
5877
  if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
@@ -5802,7 +5905,7 @@ function shouldSkipDirectory(name) {
5802
5905
  }
5803
5906
  function safeStat(path) {
5804
5907
  try {
5805
- return statSync(path);
5908
+ return statSync2(path);
5806
5909
  } catch {
5807
5910
  return null;
5808
5911
  }
@@ -5816,7 +5919,7 @@ function matchesAny(relativePath, patterns) {
5816
5919
  function matchesPattern(relativePath, pattern) {
5817
5920
  const normalizedPath = normalizePath(relativePath);
5818
5921
  const normalizedPattern = normalizePath(pattern);
5819
- const base = basename(normalizedPath);
5922
+ const base = basename2(normalizedPath);
5820
5923
  if (!normalizedPattern.includes("*")) {
5821
5924
  return normalizedPath === normalizedPattern || normalizedPath.endsWith(`/${normalizedPattern}`) || normalizedPath.includes(normalizedPattern);
5822
5925
  }
@@ -5828,12 +5931,12 @@ function wildcardToRegExp(pattern) {
5828
5931
  return new RegExp(`^${escaped}$`, "i");
5829
5932
  }
5830
5933
  function normalizePath(path) {
5831
- return path.split(sep).join("/");
5934
+ return path.split(sep3).join("/");
5832
5935
  }
5833
- function resolveUserPath(path) {
5834
- if (path === "~") return homedir2();
5835
- if (path.startsWith("~/")) return join12(homedir2(), path.slice(2));
5836
- return resolve4(path);
5936
+ function resolveUserPath2(path) {
5937
+ if (path === "~") return homedir3();
5938
+ if (path.startsWith("~/")) return join13(homedir3(), path.slice(2));
5939
+ return resolve6(path);
5837
5940
  }
5838
5941
  var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
5839
5942
  var init_connectors = __esm({
@@ -6044,17 +6147,17 @@ __export(store_exports2, {
6044
6147
  rewriteJsonl: () => rewriteJsonl,
6045
6148
  scrubText: () => scrubText
6046
6149
  });
6047
- import { existsSync as existsSync12, readFileSync as readFileSync12, appendFileSync as appendFileSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync10 } from "fs";
6048
- import { join as join13 } from "path";
6049
- import { randomUUID as randomUUID4 } from "crypto";
6150
+ import { existsSync as existsSync13, readFileSync as readFileSync13, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync11 } from "fs";
6151
+ import { join as join14 } from "path";
6152
+ import { randomUUID as randomUUID5 } from "crypto";
6050
6153
  function memPath(file) {
6051
- return join13(getMemoryDir(), file);
6154
+ return join14(getMemoryDir(), file);
6052
6155
  }
6053
6156
  function readJsonl(file) {
6054
6157
  const path = memPath(file);
6055
- if (!existsSync12(path)) return [];
6158
+ if (!existsSync13(path)) return [];
6056
6159
  const out = [];
6057
- for (const line of readFileSync12(path, "utf-8").split("\n")) {
6160
+ for (const line of readFileSync13(path, "utf-8").split("\n")) {
6058
6161
  const trimmed = line.trim();
6059
6162
  if (!trimmed) continue;
6060
6163
  try {
@@ -6066,13 +6169,13 @@ function readJsonl(file) {
6066
6169
  }
6067
6170
  function appendJsonl(file, obj) {
6068
6171
  try {
6069
- appendFileSync3(memPath(file), JSON.stringify(obj) + "\n");
6172
+ appendFileSync4(memPath(file), JSON.stringify(obj) + "\n");
6070
6173
  } catch {
6071
6174
  }
6072
6175
  }
6073
6176
  function rewriteJsonl(file, rows) {
6074
6177
  try {
6075
- writeFileSync10(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
6178
+ writeFileSync11(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
6076
6179
  } catch {
6077
6180
  }
6078
6181
  }
@@ -6081,7 +6184,7 @@ function scrubText(text) {
6081
6184
  }
6082
6185
  function addFact(input) {
6083
6186
  const fact = {
6084
- id: randomUUID4(),
6187
+ id: randomUUID5(),
6085
6188
  text: scrubText(input.text),
6086
6189
  kind: input.kind ?? "fact",
6087
6190
  source: input.source ?? "user",
@@ -6108,7 +6211,7 @@ function summarizeAnswer(answer) {
6108
6211
  }
6109
6212
  function recordAnalysis(input) {
6110
6213
  const entry = {
6111
- id: randomUUID4(),
6214
+ id: randomUUID5(),
6112
6215
  question: scrubText(input.question).slice(0, 300),
6113
6216
  summary: scrubText(summarizeAnswer(input.answer)),
6114
6217
  tools: input.tools,
@@ -6138,9 +6241,9 @@ function loadWinSnippets() {
6138
6241
  try {
6139
6242
  const dir = getWinsDir();
6140
6243
  const out = [];
6141
- for (const name of readdirSync3(dir)) {
6244
+ for (const name of readdirSync4(dir)) {
6142
6245
  if (!name.endsWith(".md") || name.toLowerCase() === "readme.md") continue;
6143
- const raw = readFileSync12(join13(dir, name), "utf-8");
6246
+ const raw = readFileSync13(join14(dir, name), "utf-8");
6144
6247
  const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? name.replace(/\.md$/, "");
6145
6248
  const body = raw.replace(/^#.*$/m, "").replace(/\s+/g, " ").trim().slice(0, 300);
6146
6249
  out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });
@@ -6342,7 +6445,7 @@ async function distillSessionFactsWithTimeout(ctx, sessionId, timeoutMs = DISTIL
6342
6445
  });
6343
6446
  const raced = await Promise.race([
6344
6447
  work,
6345
- new Promise((resolve8) => setTimeout(() => resolve8(-1), timeoutMs))
6448
+ new Promise((resolve10) => setTimeout(() => resolve10(-1), timeoutMs))
6346
6449
  ]);
6347
6450
  if (raced >= 0) return { count: raced, background };
6348
6451
  if (settled) return { count: await background, background };
@@ -6420,10 +6523,10 @@ __export(context_exports, {
6420
6523
  setPrimaryLens: () => setPrimaryLens,
6421
6524
  transcriptPathForSession: () => transcriptPathForSession
6422
6525
  });
6423
- import { basename as basename2, join as join14, resolve as resolve5, sep as sep2 } from "path";
6424
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, writeFileSync as writeFileSync11, readFileSync as readFileSync13, readdirSync as readdirSync4, statSync as statSync2, rmSync as rmSync3 } from "fs";
6425
- import { homedir as homedir3 } from "os";
6426
- import { randomUUID as randomUUID5 } from "crypto";
6526
+ import { basename as basename3, join as join15, resolve as resolve7, sep as sep4 } from "path";
6527
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync12, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync3, rmSync as rmSync4 } from "fs";
6528
+ import { homedir as homedir4 } from "os";
6529
+ import { randomUUID as randomUUID6 } from "crypto";
6427
6530
  function isSessionStale(s) {
6428
6531
  return Date.now() - s.mtime > STALE_SESSION_MS;
6429
6532
  }
@@ -6436,35 +6539,35 @@ function isAnalysisReady(ctx) {
6436
6539
  return Object.values(counts).some((n) => n > 0);
6437
6540
  }
6438
6541
  function ntrpHomeDir() {
6439
- return process.env.NTRP_HOME ? resolve5(process.env.NTRP_HOME) : join14(homedir3(), ".ntrp");
6542
+ return process.env.NTRP_HOME ? resolve7(process.env.NTRP_HOME) : join15(homedir4(), ".ntrp");
6440
6543
  }
6441
6544
  function getSessionsDir() {
6442
- const dir = join14(ntrpHomeDir(), "sessions");
6443
- if (!existsSync13(dir)) {
6444
- mkdirSync5(dir, { recursive: true });
6545
+ const dir = join15(ntrpHomeDir(), "sessions");
6546
+ if (!existsSync14(dir)) {
6547
+ mkdirSync6(dir, { recursive: true });
6445
6548
  }
6446
6549
  return dir;
6447
6550
  }
6448
6551
  function getDatasetsDir() {
6449
- const dir = join14(ntrpHomeDir(), "datasets");
6450
- if (!existsSync13(dir)) {
6451
- mkdirSync5(dir, { recursive: true });
6552
+ const dir = join15(ntrpHomeDir(), "datasets");
6553
+ if (!existsSync14(dir)) {
6554
+ mkdirSync6(dir, { recursive: true });
6452
6555
  }
6453
6556
  return dir;
6454
6557
  }
6455
6558
  function datasetPathForSession(id) {
6456
- return join14(getDatasetsDir(), `${id}.duckdb`);
6559
+ return join15(getDatasetsDir(), `${id}.duckdb`);
6457
6560
  }
6458
6561
  function transcriptPathForSession(id) {
6459
- return join14(getSessionsDir(), `${id}.transcript.md`);
6562
+ return join15(getSessionsDir(), `${id}.transcript.md`);
6460
6563
  }
6461
6564
  function contextDocPathForSession(id) {
6462
- return join14(getSessionsDir(), `${id}.context.md`);
6565
+ return join15(getSessionsDir(), `${id}.context.md`);
6463
6566
  }
6464
6567
  function makeSessionId() {
6465
6568
  const now2 = /* @__PURE__ */ new Date();
6466
6569
  const date = now2.toISOString().slice(0, 10);
6467
- const uuid2 = randomUUID5().slice(0, 4);
6570
+ const uuid2 = randomUUID6().slice(0, 4);
6468
6571
  return `${date}-${uuid2}`;
6469
6572
  }
6470
6573
  function isValidSessionId(id) {
@@ -6472,14 +6575,14 @@ function isValidSessionId(id) {
6472
6575
  }
6473
6576
  function sessionPathForId(id) {
6474
6577
  if (!isValidSessionId(id)) return null;
6475
- const dir = resolve5(getSessionsDir());
6476
- const filePath = resolve5(dir, `${id}.json`);
6477
- if (filePath !== dir && !filePath.startsWith(dir + sep2)) return null;
6578
+ const dir = resolve7(getSessionsDir());
6579
+ const filePath = resolve7(dir, `${id}.json`);
6580
+ if (filePath !== dir && !filePath.startsWith(dir + sep4)) return null;
6478
6581
  return filePath;
6479
6582
  }
6480
6583
  function initContext(oneShot, execution) {
6481
6584
  const sessionId = makeSessionId();
6482
- const sessionFile = join14(getSessionsDir(), `${sessionId}.json`);
6585
+ const sessionFile = join15(getSessionsDir(), `${sessionId}.json`);
6483
6586
  return {
6484
6587
  sessionId,
6485
6588
  sessionFile,
@@ -6593,7 +6696,7 @@ function recordMessage(ctx, role, content) {
6593
6696
  ctx.messages.push(msg);
6594
6697
  if (ctx.oneShot) return;
6595
6698
  try {
6596
- writeFileSync11(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
6699
+ writeFileSync12(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
6597
6700
  } catch {
6598
6701
  }
6599
6702
  writeSessionContextDoc(ctx);
@@ -6601,7 +6704,7 @@ function recordMessage(ctx, role, content) {
6601
6704
  function saveSessionState(ctx) {
6602
6705
  if (ctx.oneShot) return;
6603
6706
  try {
6604
- writeFileSync11(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
6707
+ writeFileSync12(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
6605
6708
  } catch {
6606
6709
  }
6607
6710
  writeSessionContextDoc(ctx);
@@ -6610,9 +6713,9 @@ function getLastActivityRelative() {
6610
6713
  const dir = getSessionsDir();
6611
6714
  let mostRecent = 0;
6612
6715
  try {
6613
- for (const name of readdirSync4(dir)) {
6716
+ for (const name of readdirSync5(dir)) {
6614
6717
  if (!name.endsWith(".json")) continue;
6615
- const m = statSync2(join14(dir, name)).mtimeMs;
6718
+ const m = statSync3(join15(dir, name)).mtimeMs;
6616
6719
  if (m > mostRecent) mostRecent = m;
6617
6720
  }
6618
6721
  } catch {
@@ -6638,7 +6741,7 @@ function loadSessionFile(id) {
6638
6741
  const filePath = sessionPathForId(id);
6639
6742
  if (!filePath) return null;
6640
6743
  try {
6641
- const raw = readFileSync13(filePath, "utf-8");
6744
+ const raw = readFileSync14(filePath, "utf-8");
6642
6745
  const session = JSON.parse(raw);
6643
6746
  if (session.thread?.length) {
6644
6747
  session.thread = normalizeThread(session.thread);
@@ -6652,16 +6755,16 @@ function listSessions(opts) {
6652
6755
  const dir = getSessionsDir();
6653
6756
  const entries = [];
6654
6757
  try {
6655
- const files = readdirSync4(dir).filter((name) => name.endsWith(".json")).map((name) => {
6656
- const filePath = join14(dir, name);
6657
- return { name, filePath, mtime: statSync2(filePath).mtimeMs };
6758
+ const files = readdirSync5(dir).filter((name) => name.endsWith(".json")).map((name) => {
6759
+ const filePath = join15(dir, name);
6760
+ return { name, filePath, mtime: statSync3(filePath).mtimeMs };
6658
6761
  }).sort((a, b) => b.mtime - a.mtime);
6659
6762
  const filesToRead = opts?.limit ? files.slice(0, opts.limit) : files;
6660
6763
  for (const { name, filePath, mtime } of filesToRead) {
6661
- const id = basename2(name, ".json");
6764
+ const id = basename3(name, ".json");
6662
6765
  if (!isValidSessionId(id)) continue;
6663
6766
  try {
6664
- const raw = readFileSync13(filePath, "utf-8");
6767
+ const raw = readFileSync14(filePath, "utf-8");
6665
6768
  const session = JSON.parse(raw);
6666
6769
  entries.push({
6667
6770
  id,
@@ -6723,7 +6826,7 @@ async function closeAllActiveSessions(ctx) {
6723
6826
  skipped.push(s.id);
6724
6827
  continue;
6725
6828
  }
6726
- writeFileSync11(filePath, JSON.stringify(file, null, 2) + "\n");
6829
+ writeFileSync12(filePath, JSON.stringify(file, null, 2) + "\n");
6727
6830
  writeContextDocForSessionFile(file);
6728
6831
  closed.push(s.id);
6729
6832
  }
@@ -6763,7 +6866,7 @@ async function rotateToFreshSession(ctx) {
6763
6866
  const newId = makeSessionId();
6764
6867
  resetContextForSwitch(ctx, {
6765
6868
  sessionId: newId,
6766
- sessionFile: join14(getSessionsDir(), `${newId}.json`),
6869
+ sessionFile: join15(getSessionsDir(), `${newId}.json`),
6767
6870
  messages: [],
6768
6871
  stage: "new",
6769
6872
  analysis: defaultSessionAnalysis(),
@@ -6802,14 +6905,14 @@ async function finalizeSession(ctx, stage) {
6802
6905
  }
6803
6906
  for (const path of [ctx.datasetPath, `${ctx.datasetPath}.wal`]) {
6804
6907
  try {
6805
- rmSync3(path, { force: true });
6908
+ rmSync4(path, { force: true });
6806
6909
  } catch {
6807
6910
  }
6808
6911
  }
6809
6912
  }
6810
6913
  discardSessionTranscript(ctx.sessionId);
6811
6914
  try {
6812
- rmSync3(contextDocPathForSession(ctx.sessionId), { force: true });
6915
+ rmSync4(contextDocPathForSession(ctx.sessionId), { force: true });
6813
6916
  } catch {
6814
6917
  }
6815
6918
  return void 0;
@@ -6864,7 +6967,7 @@ async function finalizeSession(ctx, stage) {
6864
6967
  file.pending_ask = ctx.pendingAsk;
6865
6968
  }
6866
6969
  try {
6867
- writeFileSync11(ctx.sessionFile, JSON.stringify(file, null, 2) + "\n");
6970
+ writeFileSync12(ctx.sessionFile, JSON.stringify(file, null, 2) + "\n");
6868
6971
  } catch {
6869
6972
  }
6870
6973
  writeContextDocForSessionFile(file, { snapshot: ctx.snapshot.computeResult });
@@ -7323,9 +7426,9 @@ var init_tool_schemas = __esm({
7323
7426
  });
7324
7427
 
7325
7428
  // src/ai/privacy.ts
7326
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, appendFileSync as appendFileSync4 } from "fs";
7327
- import { homedir as homedir4 } from "os";
7328
- import { join as join15 } from "path";
7429
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, appendFileSync as appendFileSync5 } from "fs";
7430
+ import { homedir as homedir5 } from "os";
7431
+ import { join as join16 } from "path";
7329
7432
  function stripPII(obj) {
7330
7433
  if (obj === null || obj === void 0) return obj;
7331
7434
  if (typeof obj !== "object") return obj;
@@ -7340,15 +7443,15 @@ function stripPII(obj) {
7340
7443
  return out;
7341
7444
  }
7342
7445
  function ensureAuditDir() {
7343
- if (!existsSync14(AUDIT_DIR)) {
7344
- mkdirSync6(AUDIT_DIR, { recursive: true });
7446
+ if (!existsSync15(AUDIT_DIR)) {
7447
+ mkdirSync7(AUDIT_DIR, { recursive: true });
7345
7448
  }
7346
7449
  }
7347
7450
  function logToolCall(entry) {
7348
7451
  ensureAuditDir();
7349
7452
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7350
- const path = join15(AUDIT_DIR, `agentic-${date}.jsonl`);
7351
- appendFileSync4(path, JSON.stringify(entry) + "\n");
7453
+ const path = join16(AUDIT_DIR, `agentic-${date}.jsonl`);
7454
+ appendFileSync5(path, JSON.stringify(entry) + "\n");
7352
7455
  }
7353
7456
  var PII_FIELDS, AUDIT_DIR;
7354
7457
  var init_privacy = __esm({
@@ -7371,7 +7474,7 @@ var init_privacy = __esm({
7371
7474
  "raw_data",
7372
7475
  "metadata"
7373
7476
  ]);
7374
- AUDIT_DIR = join15(homedir4(), ".ntrp", "audit");
7477
+ AUDIT_DIR = join16(homedir5(), ".ntrp", "audit");
7375
7478
  }
7376
7479
  });
7377
7480
 
@@ -7429,17 +7532,17 @@ __export(play_outcomes_exports, {
7429
7532
  listPlayOutcomes: () => listPlayOutcomes,
7430
7533
  recordPlayOutcomes: () => recordPlayOutcomes
7431
7534
  });
7432
- import { existsSync as existsSync15, readFileSync as readFileSync14, appendFileSync as appendFileSync5 } from "fs";
7433
- import { join as join16 } from "path";
7434
- import { randomUUID as randomUUID6 } from "crypto";
7535
+ import { existsSync as existsSync16, readFileSync as readFileSync15, appendFileSync as appendFileSync6 } from "fs";
7536
+ import { join as join17 } from "path";
7537
+ import { randomUUID as randomUUID7 } from "crypto";
7435
7538
  function outcomesPath() {
7436
- return join16(getMemoryDir(), OUTCOMES_FILE);
7539
+ return join17(getMemoryDir(), OUTCOMES_FILE);
7437
7540
  }
7438
7541
  function listPlayOutcomes() {
7439
7542
  const path = outcomesPath();
7440
- if (!existsSync15(path)) return [];
7543
+ if (!existsSync16(path)) return [];
7441
7544
  const out = [];
7442
- for (const line of readFileSync14(path, "utf-8").split("\n")) {
7545
+ for (const line of readFileSync15(path, "utf-8").split("\n")) {
7443
7546
  const trimmed = line.trim();
7444
7547
  if (!trimmed) continue;
7445
7548
  try {
@@ -7469,7 +7572,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
7469
7572
  if (seen.has(key)) continue;
7470
7573
  seen.add(key);
7471
7574
  const record = {
7472
- id: randomUUID6(),
7575
+ id: randomUUID7(),
7473
7576
  play_id: playId,
7474
7577
  strategy_slug: strategy.slug,
7475
7578
  workstream_order: outcome.workstream_order,
@@ -7482,7 +7585,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
7482
7585
  reviewed_at: (/* @__PURE__ */ new Date()).toISOString()
7483
7586
  };
7484
7587
  try {
7485
- appendFileSync5(outcomesPath(), JSON.stringify(record) + "\n");
7588
+ appendFileSync6(outcomesPath(), JSON.stringify(record) + "\n");
7486
7589
  written++;
7487
7590
  } catch {
7488
7591
  }
@@ -7743,16 +7846,16 @@ var init_metrics_benchmarks = __esm({
7743
7846
  });
7744
7847
 
7745
7848
  // src/config/profile.ts
7746
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
7747
- import { join as join17 } from "path";
7849
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
7850
+ import { join as join18 } from "path";
7748
7851
  function isProfileConfigured(profile = loadProfile()) {
7749
7852
  if (!profile) return false;
7750
7853
  return profile.company_name.trim().length > 0;
7751
7854
  }
7752
7855
  function loadProfile() {
7753
- if (!existsSync16(PROFILE_PATH)) return null;
7856
+ if (!existsSync17(PROFILE_PATH)) return null;
7754
7857
  try {
7755
- const parsed = JSON.parse(readFileSync15(PROFILE_PATH, "utf-8"));
7858
+ const parsed = JSON.parse(readFileSync16(PROFILE_PATH, "utf-8"));
7756
7859
  if (!parsed || typeof parsed !== "object") return null;
7757
7860
  return parsed;
7758
7861
  } catch {
@@ -7765,7 +7868,7 @@ var init_profile = __esm({
7765
7868
  "use strict";
7766
7869
  init_store();
7767
7870
  NTRP_DIR3 = ntrpHome();
7768
- PROFILE_PATH = join17(NTRP_DIR3, "profile.json");
7871
+ PROFILE_PATH = join18(NTRP_DIR3, "profile.json");
7769
7872
  }
7770
7873
  });
7771
7874
 
@@ -10522,7 +10625,7 @@ function createPromptSession(existing, ctx) {
10522
10625
  }
10523
10626
  process.stdout.write("\n" + prompt);
10524
10627
  try {
10525
- return await new Promise((resolve8, reject) => {
10628
+ return await new Promise((resolve10, reject) => {
10526
10629
  let value = "";
10527
10630
  let settled = false;
10528
10631
  const cleanup = () => {
@@ -10558,7 +10661,7 @@ function createPromptSession(existing, ctx) {
10558
10661
  process.stdout.write("\n");
10559
10662
  const trimmed = stripTerminalArtifacts(value).trim();
10560
10663
  assertNotGlobalReplCommand(trimmed);
10561
- resolve8(trimmed);
10664
+ resolve10(trimmed);
10562
10665
  });
10563
10666
  return;
10564
10667
  }
@@ -10574,7 +10677,7 @@ function createPromptSession(existing, ctx) {
10574
10677
  process.stdout.write("\n");
10575
10678
  const trimmed = stripTerminalArtifacts(value).trim();
10576
10679
  assertNotGlobalReplCommand(trimmed);
10577
- resolve8(trimmed);
10680
+ resolve10(trimmed);
10578
10681
  });
10579
10682
  return;
10580
10683
  }
@@ -10913,7 +11016,40 @@ Close the loop to action. Produce a markdown report, a notes export, CSV
10913
11016
  receipts, or a repository package \u2014 or generate a ready-to-paste prompt for
10914
11017
  another agent to build a review deck, an Asana project, a Clay table, or an
10915
11018
  action plan from this diagnosis. Producing an output marks the session
10916
- delivered so it stops showing up as unfinished work.`
11019
+ delivered so it stops showing up as unfinished work. Files land under
11020
+ \`export-dir\` by kind; point Claude Desktop at a folder with \`/inbox set\`.`
11021
+ },
11022
+ {
11023
+ name: "exports",
11024
+ raw: `---
11025
+ name: exports
11026
+ description: List, open, or move export files
11027
+ section: Start
11028
+ args: [list [kind]|open|move <id|file> <dest>]
11029
+ handler: ../commands/exports.ts
11030
+ ---
11031
+
11032
+ Catalog of handoffs and other deliverables. Lists recent writes from the
11033
+ durable \`manifest.jsonl\` under your export archive, prints absolute paths
11034
+ (\`open\`), and relocates files while recording the move trail so desktop AI
11035
+ apps can see where things went (\`move\`). Companion: \`/inbox\` sets the
11036
+ Claude-facing folder with stable \`latest-*\` pointers.`
11037
+ },
11038
+ {
11039
+ name: "inbox",
11040
+ raw: `---
11041
+ name: inbox
11042
+ description: Set the desktop-AI folder for handoffs
11043
+ section: Settings
11044
+ args: [show|set <path>|clear]
11045
+ handler: ../commands/exports.ts
11046
+ ---
11047
+
11048
+ Declare a folder Claude Desktop (or any desktop AI) can read. NTRP copies
11049
+ each handoff there and overwrites stable \`latest-handoff.md\` /
11050
+ \`latest-handoff-deck.md\` pointers so the app always finds the newest file.
11051
+ \`INDEX.md\` in that folder links back to the canonical archive. Does not
11052
+ delete files on \`clear\` \u2014 only removes the config pointer.`
10917
11053
  },
10918
11054
  {
10919
11055
  name: "onboard",
@@ -10959,7 +11095,8 @@ Validate local readiness or configure NTRP non-interactively for automation.
10959
11095
  \`setup check --json\` reports license, profile, API key, database, and writable
10960
11096
  directory state. \`setup agent\` accepts a profile JSON file or direct flags \u2014
10961
11097
  \`--llm-key <key>\` auto-detects the provider from any pasted key
10962
- (\`--llm-provider <id>\` to force one).`
11098
+ (\`--llm-provider <id>\` to force one), plus \`--export-dir\` and
11099
+ \`--ai-inbox-dir\` for deliverable locations.`
10963
11100
  },
10964
11101
  {
10965
11102
  name: "update",
@@ -11256,6 +11393,22 @@ handler: ../commands/progress.ts
11256
11393
  Hours saved, weekly activity trend, session counts, AI token usage, and the
11257
11394
  full milestone ladder with progress bars. Use reset (type "reset" to confirm)
11258
11395
  to clear hours and milestones while keeping this install's identity.`
11396
+ },
11397
+ {
11398
+ name: "deepdive",
11399
+ raw: `---
11400
+ name: deepdive
11401
+ description: Metric slides \u2014 what each number means
11402
+ section: Navigation
11403
+ args: [<metric>|list|tour]
11404
+ handler: ../commands/deepdive.ts
11405
+ ---
11406
+
11407
+ CLI slide deck for every vital sign and SaaS metric: definition, formula,
11408
+ visual, and dollar translation. Bare \`/deepdive\` runs the onboarding tour
11409
+ (SaaS refresher + five vitals). \`/deepdive <metric>\` jumps to one slide.
11410
+ \`/deepdive list\` prints the catalog. Works without an AI key. Re-run anytime
11411
+ from the homescreen \u2014 live values overlay when an analysis exists.`
11259
11412
  },
11260
11413
  {
11261
11414
  name: "status",
@@ -11434,7 +11587,7 @@ handler: ../commands/config.ts
11434
11587
  Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
11435
11588
  \`api-key\` (Anthropic), \`openai-api-key\` (and \`groq-api-key\`, \`google-api-key\`, ...),
11436
11589
  \`llm-primary\` (default engine), \`llm-tier\`, \`llm-auto-failover\`,
11437
- \`default-format\`, \`export-dir\`.
11590
+ \`default-format\`, \`export-dir\`, \`ai-inbox-dir\` (or use \`/inbox set\`).
11438
11591
 
11439
11592
  Setting a provider key opens a hidden prompt and auto-discovers that
11440
11593
  provider's models. Prefer \`/connect\` \u2014 it detects the provider for you.`
@@ -11540,8 +11693,8 @@ paragraph that flows into all AI surfaces.`
11540
11693
  });
11541
11694
 
11542
11695
  // src/ai/prompt-parts.ts
11543
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
11544
- import { join as join18 } from "path";
11696
+ import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
11697
+ import { join as join19 } from "path";
11545
11698
  function buildCompanyProfileBlock() {
11546
11699
  const p = loadProfile();
11547
11700
  if (!p) return "";
@@ -11560,10 +11713,10 @@ function buildCompanyProfileBlock() {
11560
11713
  return lines.join("\n");
11561
11714
  }
11562
11715
  function loadAnalystFile() {
11563
- const path = join18(ntrpHome(), ANALYST_FILE_NAME);
11716
+ const path = join19(ntrpHome(), ANALYST_FILE_NAME);
11564
11717
  try {
11565
- if (!existsSync17(path)) return null;
11566
- const raw = readFileSync16(path, "utf-8").trim();
11718
+ if (!existsSync18(path)) return null;
11719
+ const raw = readFileSync17(path, "utf-8").trim();
11567
11720
  if (!raw) return null;
11568
11721
  if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
11569
11722
  const head = raw.slice(0, Math.floor(ANALYST_FILE_MAX_CHARS * 0.75));
@@ -12273,8 +12426,25 @@ var init_llm_attribution = __esm({
12273
12426
  }
12274
12427
  });
12275
12428
 
12276
- // src/output/terminal.ts
12429
+ // src/ui/slides.ts
12277
12430
  import chalk9 from "chalk";
12431
+ function printDeepdiveHint(metricId, label) {
12432
+ const name = label ?? metricId;
12433
+ console.log(
12434
+ " " + chalk9.dim("How this number works: ") + paint("accent", `/deepdive ${metricId}`) + chalk9.dim(` \u2014 ${name}`)
12435
+ );
12436
+ console.log();
12437
+ }
12438
+ var init_slides = __esm({
12439
+ "src/ui/slides.ts"() {
12440
+ "use strict";
12441
+ init_theme();
12442
+ init_layout();
12443
+ }
12444
+ });
12445
+
12446
+ // src/output/terminal.ts
12447
+ import chalk10 from "chalk";
12278
12448
  import Table2 from "cli-table3";
12279
12449
  function centerPad(text, width) {
12280
12450
  if (text.length >= width) return text;
@@ -12293,7 +12463,7 @@ function statusBadge(status) {
12293
12463
  }
12294
12464
  }
12295
12465
  function printHeading(label, detail) {
12296
- console.log(` ${sectionHeading(label)}${detail ? chalk9.dim(` ${detail}`) : ""}`);
12466
+ console.log(` ${sectionHeading(label)}${detail ? chalk10.dim(` ${detail}`) : ""}`);
12297
12467
  }
12298
12468
  function printResultCard(title, rows) {
12299
12469
  const width = resolveCardWidth({ min: 60, max: 100, margin: 4 });
@@ -12314,32 +12484,39 @@ function printVitalSignRow(vs) {
12314
12484
  const label = VITAL_SIGN_LABELS[vs.vital_sign].padEnd(18);
12315
12485
  const bar = scoreBar(vs.score, vs.status);
12316
12486
  const score = String(Math.round(vs.score)).padStart(4);
12317
- const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk9.dim(vs.dollar_label ?? "")}` : chalk9.dim("\u2014");
12318
- console.log(` ${dot} ${label} ${bar} ${chalk9.bold(score)} ${chalk9.dim("\u2502")} ${impact}`);
12487
+ const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk10.dim(vs.dollar_label ?? "")}` : chalk10.dim("\u2014");
12488
+ console.log(` ${dot} ${label} ${bar} ${chalk10.bold(score)} ${chalk10.dim("\u2502")} ${impact}`);
12319
12489
  }
12320
12490
  function printHealthSummary(result, _pipelineMetrics) {
12321
- const scoreStr = `${chalk9.bold(String(Math.round(result.overall_score)))}${chalk9.dim("/100")}`;
12322
- const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk9.dim("total at risk")}` : chalk9.dim("No dollar-weighted risk detected");
12491
+ const scoreStr = `${chalk10.bold(String(Math.round(result.overall_score)))}${chalk10.dim("/100")}`;
12492
+ const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk10.dim("total at risk")}` : chalk10.dim("No dollar-weighted risk detected");
12323
12493
  const next = result.overall_status === "red" ? actionHint("Next:", "/playbook", "review recommended plays") : result.overall_status === "yellow" ? actionHint("Next:", "/diagnose --deep", "investigate the weak signal") : actionHint("Next:", "/report", "export the clean snapshot");
12324
12494
  printResultCard("Overall Health", [
12325
- `${chalk9.dim("Score")} ${scoreStr} ${statusBadge(result.overall_status)}`,
12326
- `${chalk9.dim("Held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`,
12327
- `${chalk9.dim("Revenue")} ${impact}`,
12495
+ `${chalk10.dim("Score")} ${scoreStr} ${statusBadge(result.overall_status)}`,
12496
+ `${chalk10.dim("Held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`,
12497
+ `${chalk10.dim("Revenue")} ${impact}`,
12328
12498
  next
12329
12499
  ]);
12500
+ printDeepdiveHint(
12501
+ result.gating_vital_sign,
12502
+ VITAL_SIGN_LABELS[result.gating_vital_sign]
12503
+ );
12330
12504
  }
12331
12505
  function printHealthLine(result) {
12332
- const score = `${chalk9.bold(String(Math.round(result.overall_score)))}${chalk9.dim("/100")}`;
12506
+ const score = `${chalk10.bold(String(Math.round(result.overall_score)))}${chalk10.dim("/100")}`;
12333
12507
  const parts = [
12334
- `${chalk9.dim("Health")} ${score} ${statusBadge(result.overall_status)}`,
12335
- `${chalk9.dim("held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`
12508
+ `${chalk10.dim("Health")} ${score} ${statusBadge(result.overall_status)}`,
12509
+ `${chalk10.dim("held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`
12336
12510
  ];
12337
12511
  if (result.total_value_at_risk != null && result.total_value_at_risk > 0) {
12338
- parts.push(`${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk9.dim("total at risk")}`);
12512
+ parts.push(`${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk10.dim("total at risk")}`);
12339
12513
  }
12340
12514
  console.log();
12341
- console.log(" " + parts.join(chalk9.dim(" \xB7 ")));
12342
- console.log();
12515
+ console.log(" " + parts.join(chalk10.dim(" \xB7 ")));
12516
+ printDeepdiveHint(
12517
+ result.gating_vital_sign,
12518
+ VITAL_SIGN_LABELS[result.gating_vital_sign]
12519
+ );
12343
12520
  }
12344
12521
  function printVitalSigns(vitals) {
12345
12522
  console.log();
@@ -12358,8 +12535,8 @@ function printSegmentSummary(segments) {
12358
12535
  const dot = statusDot(seg.result.overall_status);
12359
12536
  const name = seg.segment.name.padEnd(24);
12360
12537
  const score = String(Math.round(seg.result.overall_score)).padStart(4);
12361
- const gating = chalk9.dim(VITAL_SIGN_LABELS[seg.result.gating_vital_sign]);
12362
- console.log(` ${dot} ${name} ${chalk9.bold(score)} ${chalk9.dim("\u2502")} ${gating}`);
12538
+ const gating = chalk10.dim(VITAL_SIGN_LABELS[seg.result.gating_vital_sign]);
12539
+ console.log(` ${dot} ${name} ${chalk10.bold(score)} ${chalk10.dim("\u2502")} ${gating}`);
12363
12540
  }
12364
12541
  console.log();
12365
12542
  }
@@ -12384,7 +12561,7 @@ function printTopProblems(segments, limit = 7, opts = {}) {
12384
12561
  if (opts.compact) return;
12385
12562
  printHeading("Top Problems");
12386
12563
  console.log();
12387
- console.log(" " + chalk9.dim("No dollar-weighted problems found across segments."));
12564
+ console.log(" " + chalk10.dim("No dollar-weighted problems found across segments."));
12388
12565
  console.log();
12389
12566
  return;
12390
12567
  }
@@ -12399,12 +12576,12 @@ function printTopProblems(segments, limit = 7, opts = {}) {
12399
12576
  for (let i = 0; i < top.length; i++) {
12400
12577
  const p = top[i];
12401
12578
  console.log(
12402
- ` ${statusDot(p.status)} ${p.segment.padEnd(segW)} ${p.vitalSignLabel.padEnd(vitalW)} ${paint("success", dollarStrs[i].padStart(dollarW))} ${chalk9.dim(p.dollarLabel)}`
12579
+ ` ${statusDot(p.status)} ${p.segment.padEnd(segW)} ${p.vitalSignLabel.padEnd(vitalW)} ${paint("success", dollarStrs[i].padStart(dollarW))} ${chalk10.dim(p.dollarLabel)}`
12403
12580
  );
12404
12581
  }
12405
12582
  if (problems.length > top.length) {
12406
12583
  console.log(
12407
- " " + chalk9.dim(`${problems.length - top.length} more \u2014 `) + paint("accent", "/diagnose") + chalk9.dim(" for the full report")
12584
+ " " + chalk10.dim(`${problems.length - top.length} more \u2014 `) + paint("accent", "/diagnose") + chalk10.dim(" for the full report")
12408
12585
  );
12409
12586
  }
12410
12587
  console.log();
@@ -12413,14 +12590,14 @@ function printTopProblems(segments, limit = 7, opts = {}) {
12413
12590
  const labelW = Math.max("".length, ...top.map((p) => p.dollarLabel.length));
12414
12591
  const impactW = dollarW + 2 + labelW;
12415
12592
  console.log(
12416
- ` ${sectionHeading("Top Problems")}` + chalk9.dim(` (${top.length} of ${problems.length})`)
12593
+ ` ${sectionHeading("Top Problems")}` + chalk10.dim(` (${top.length} of ${problems.length})`)
12417
12594
  );
12418
12595
  console.log();
12419
12596
  const segColW = 2 + segW;
12420
12597
  const hSeg = centerPad("Segment", segColW);
12421
12598
  const hVital = centerPad("Vital Sign", vitalW);
12422
12599
  const hImpact = centerPad("Revenue Impact", Math.max(impactW, "Revenue Impact".length));
12423
- console.log(` ${chalk9.dim(hSeg)} ${chalk9.dim(hVital)} ${chalk9.dim(hImpact)}`);
12600
+ console.log(` ${chalk10.dim(hSeg)} ${chalk10.dim(hVital)} ${chalk10.dim(hImpact)}`);
12424
12601
  console.log();
12425
12602
  for (let i = 0; i < top.length; i++) {
12426
12603
  const p = top[i];
@@ -12428,39 +12605,46 @@ function printTopProblems(segments, limit = 7, opts = {}) {
12428
12605
  const seg = p.segment.padEnd(segW);
12429
12606
  const vital = p.vitalSignLabel.padEnd(vitalW);
12430
12607
  const dollar = paint("success", dollarStrs[i].padStart(dollarW));
12431
- const label = chalk9.dim(p.dollarLabel);
12608
+ const label = chalk10.dim(p.dollarLabel);
12432
12609
  console.log(` ${dot} ${seg} ${vital} ${dollar} ${label}`);
12433
12610
  }
12434
12611
  if (problems.length > top.length) {
12435
12612
  console.log();
12436
- console.log(` ${chalk9.dim("Run /diagnose --segment <name> to drill in")}`);
12613
+ console.log(` ${chalk10.dim("Run /diagnose --segment <name> to drill in")}`);
12437
12614
  }
12438
12615
  console.log();
12439
12616
  }
12440
12617
  function printFindingCard(finding) {
12441
12618
  const dot = severityPaint(finding.severity)("\u25CF");
12442
- const dollarTag = finding.dollar_value != null && finding.dollar_value > 0 ? ` ${chalk9.dim("\xB7")} ${paint("success", formatDollarValue(finding.dollar_value))}` : "";
12443
- console.log(` ${dot} ${chalk9.bold(finding.segment)}${dollarTag}`);
12619
+ const dollarTag = finding.dollar_value != null && finding.dollar_value > 0 ? ` ${chalk10.dim("\xB7")} ${paint("success", formatDollarValue(finding.dollar_value))}` : "";
12620
+ console.log(` ${dot} ${chalk10.bold(finding.segment)}${dollarTag}`);
12444
12621
  printMarkdown(finding.finding, { indent: 2 });
12445
12622
  if (finding.recommended_plays && finding.recommended_plays.length > 0) {
12446
12623
  for (const play of finding.recommended_plays) {
12447
12624
  console.log(
12448
- ` ${chalk9.dim("Consider:")} ${paint("accent", play.play_name)} ${chalk9.dim("\u2192")} ${chalk9.dim("/playbook " + play.play_id)}`
12625
+ ` ${chalk10.dim("Consider:")} ${paint("accent", play.play_name)} ${chalk10.dim("\u2192")} ${chalk10.dim("/playbook " + play.play_id)}`
12449
12626
  );
12450
12627
  }
12451
12628
  }
12452
- console.log();
12629
+ if (finding.recommended_focus) {
12630
+ printDeepdiveHint(
12631
+ finding.recommended_focus,
12632
+ VITAL_SIGN_LABELS[finding.recommended_focus]
12633
+ );
12634
+ } else {
12635
+ console.log();
12636
+ }
12453
12637
  }
12454
12638
  function printFindings(findings) {
12455
12639
  if (findings.length === 0) {
12456
- console.log(chalk9.dim(" No findings generated."));
12640
+ console.log(chalk10.dim(" No findings generated."));
12457
12641
  return;
12458
12642
  }
12459
12643
  for (const finding of findings) printFindingCard(finding);
12460
12644
  }
12461
12645
  function printEntityCounts(counts) {
12462
12646
  const table = new Table2({
12463
- head: [chalk9.dim("Entity"), chalk9.dim("Count")],
12647
+ head: [chalk10.dim("Entity"), chalk10.dim("Count")],
12464
12648
  colWidths: [20, 12],
12465
12649
  style: { head: [], border: [] }
12466
12650
  });
@@ -12475,7 +12659,7 @@ function printSegmentDetail(seg, aggregate) {
12475
12659
  console.log();
12476
12660
  printHeading(seg.segment.name);
12477
12661
  console.log(
12478
- ` ${statusDot(seg.result.overall_status)} ${color(chalk9.bold(formatScore(seg.result.overall_score)))}${chalk9.dim("/100")} ${chalk9.dim("Held back by:")} ${paint("accent", VITAL_SIGN_LABELS[seg.result.gating_vital_sign])}`
12662
+ ` ${statusDot(seg.result.overall_status)} ${color(chalk10.bold(formatScore(seg.result.overall_score)))}${chalk10.dim("/100")} ${chalk10.dim("Held back by:")} ${paint("accent", VITAL_SIGN_LABELS[seg.result.gating_vital_sign])}`
12479
12663
  );
12480
12664
  console.log();
12481
12665
  for (const vs of seg.result.vital_signs) {
@@ -12486,8 +12670,8 @@ function printSegmentDetail(seg, aggregate) {
12486
12670
  const label = VITAL_SIGN_LABELS[vs.vital_sign].padEnd(18);
12487
12671
  const bar = scoreBar(vs.score, vs.status);
12488
12672
  const score = String(Math.round(vs.score)).padStart(4);
12489
- const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk9.dim(vs.dollar_label ?? "")}` : chalk9.dim("\u2014");
12490
- console.log(` ${dot} ${label} ${bar} ${chalk9.bold(score)} ${padLeft(deltaStr, 4)} ${chalk9.dim("\u2502")} ${impact}`);
12673
+ const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk10.dim(vs.dollar_label ?? "")}` : chalk10.dim("\u2014");
12674
+ console.log(` ${dot} ${label} ${bar} ${chalk10.bold(score)} ${padLeft(deltaStr, 4)} ${chalk10.dim("\u2502")} ${impact}`);
12491
12675
  }
12492
12676
  console.log();
12493
12677
  }
@@ -12598,12 +12782,12 @@ async function renderDiagnoseStream(options) {
12598
12782
  console.log();
12599
12783
  }
12600
12784
  if (collectedFindings.length === 0) {
12601
- console.log(chalk9.dim(" No findings generated."));
12785
+ console.log(chalk10.dim(" No findings generated."));
12602
12786
  console.log();
12603
12787
  }
12604
12788
  if (toolCalls > 0) {
12605
12789
  console.log(
12606
- chalk9.dim(` Investigated with ${toolCalls} tool call${toolCalls === 1 ? "" : "s"}`)
12790
+ chalk10.dim(` Investigated with ${toolCalls} tool call${toolCalls === 1 ? "" : "s"}`)
12607
12791
  );
12608
12792
  console.log();
12609
12793
  }
@@ -12624,7 +12808,7 @@ async function renderDiagnoseStream(options) {
12624
12808
  });
12625
12809
  } catch (err) {
12626
12810
  findingsSpinner.fail(deep ? "Agentic investigation failed" : "AI findings failed");
12627
- console.error(chalk9.dim(String(err)));
12811
+ console.error(chalk10.dim(String(err)));
12628
12812
  }
12629
12813
  }
12630
12814
  return { fullResult, findings: collectedFindings };
@@ -12638,14 +12822,14 @@ function printMetricsTable(metrics, groupOrder) {
12638
12822
  for (const m of groupMetrics) {
12639
12823
  const dot = m.unavailable_reason ? statusDot("neutral") : statusDot(m.status);
12640
12824
  const label = m.label.padEnd(28);
12641
- const valueStr = m.unavailable_reason ? chalk9.dim("--") : chalk9.bold(m.formatted);
12825
+ const valueStr = m.unavailable_reason ? chalk10.dim("--") : chalk10.bold(m.formatted);
12642
12826
  const confTag = m.confidence != null && m.confidence < 80 && m.confidence_label ? paint("warning", ` ${m.confidence_label} (${m.confidence})`) : "";
12643
- const note = m.unavailable_reason ? chalk9.dim(m.unavailable_reason) : m.benchmark_note ? chalk9.dim(m.benchmark_note) : "";
12827
+ const note = m.unavailable_reason ? chalk10.dim(m.unavailable_reason) : m.benchmark_note ? chalk10.dim(m.benchmark_note) : "";
12644
12828
  console.log(` ${dot} ${label} ${valueStr}${confTag}${note ? " " + note : ""}`);
12645
12829
  if (m.reliability_gate?.requirements?.length && (m.confidence ?? 100) < 80) {
12646
12830
  const gate = m.reliability_gate.requirements[0];
12647
12831
  if (gate) {
12648
- console.log(chalk9.dim(` \u2514 Gate: ${gate}`));
12832
+ console.log(chalk10.dim(` \u2514 Gate: ${gate}`));
12649
12833
  }
12650
12834
  }
12651
12835
  }
@@ -12661,6 +12845,7 @@ var init_terminal = __esm({
12661
12845
  init_theme();
12662
12846
  init_layout();
12663
12847
  init_llm_attribution();
12848
+ init_slides();
12664
12849
  }
12665
12850
  });
12666
12851
 
@@ -12670,15 +12855,30 @@ __export(metrics_report_exports, {
12670
12855
  printMetricsNextSteps: () => printMetricsNextSteps,
12671
12856
  renderMetricsReport: () => renderMetricsReport
12672
12857
  });
12673
- import chalk10 from "chalk";
12858
+ import chalk11 from "chalk";
12859
+ function pickDeepdiveMetric(metrics) {
12860
+ const rank = (s) => s === "red" ? 0 : s === "yellow" ? 1 : s === "green" ? 2 : 3;
12861
+ const core = ["nrr", "arr", "grr", "pipeline_coverage", "win_rate", "pipeline_velocity"];
12862
+ const coreRank = (id) => {
12863
+ const i = core.indexOf(id);
12864
+ return i === -1 ? 99 : i;
12865
+ };
12866
+ const usable = metrics.filter((m) => m.value != null);
12867
+ if (usable.length === 0) return void 0;
12868
+ return [...usable].sort((a, b) => {
12869
+ const rd = rank(a.status) - rank(b.status);
12870
+ if (rd !== 0) return rd;
12871
+ return coreRank(a.metric) - coreRank(b.metric);
12872
+ })[0];
12873
+ }
12674
12874
  function renderMetricsReport(ctx, metrics, coverage, sourceType, options = {}) {
12675
12875
  const title = options.title ?? "SaaS Metrics Analysis";
12676
12876
  const tier = coverageTier(coverage);
12677
12877
  const deterministic = buildDeterministicInsights(metrics, coverage, sourceType);
12678
12878
  const headline = pickHeadlineInsight(deterministic);
12679
12879
  console.log();
12680
- console.log(chalk10.bold(` ${title}`));
12681
- console.log(" " + chalk10.dim(formatCoverageHeader(sourceType, coverage)));
12880
+ console.log(chalk11.bold(` ${title}`));
12881
+ console.log(" " + chalk11.dim(formatCoverageHeader(sourceType, coverage)));
12682
12882
  console.log();
12683
12883
  printDataQualityPanel(coverage, sourceType, tier);
12684
12884
  if (options.snapshot && coverage.distinct_quarters >= 2) {
@@ -12686,17 +12886,21 @@ function renderMetricsReport(ctx, metrics, coverage, sourceType, options = {}) {
12686
12886
  }
12687
12887
  if (headline) {
12688
12888
  console.log(" " + paint("warning", "\u25B8 Headline"));
12689
- console.log(" " + chalk10.white(wrapInsight(headline)));
12889
+ console.log(" " + chalk11.white(wrapInsight(headline)));
12690
12890
  console.log();
12691
12891
  }
12692
12892
  printMetricsTable(metrics, GROUP_ORDER);
12893
+ const dive = pickDeepdiveMetric(metrics);
12894
+ if (dive) {
12895
+ printDeepdiveHint(dive.metric, dive.label);
12896
+ }
12693
12897
  if (deterministic.length > 0) {
12694
12898
  console.log(" " + bold("Pattern checks"));
12695
12899
  console.log();
12696
12900
  for (const insight of deterministic.slice(0, 5)) {
12697
12901
  const dot = insight.severity === "warning" ? statusDot("yellow") : insight.severity === "critical" ? statusDot("red") : statusDot("neutral");
12698
12902
  if (insight.headline) continue;
12699
- console.log(` ${dot} ${chalk10.dim(wrapInsight(insight.message))}`);
12903
+ console.log(` ${dot} ${chalk11.dim(wrapInsight(insight.message))}`);
12700
12904
  }
12701
12905
  console.log();
12702
12906
  }
@@ -12725,7 +12929,7 @@ function printDataQualityPanel(coverage, sourceType, tier) {
12725
12929
  rows.push(["Ledger", "not loaded \u2014 retention inferred from CRM"]);
12726
12930
  }
12727
12931
  for (const [label, value] of rows) {
12728
- console.log(` ${chalk10.dim(String(label).padEnd(14))} ${value}`);
12932
+ console.log(` ${chalk11.dim(String(label).padEnd(14))} ${value}`);
12729
12933
  }
12730
12934
  console.log();
12731
12935
  }
@@ -12736,10 +12940,10 @@ function printCloseTrend(snapshot, cadence) {
12736
12940
  console.log(" " + bold(`Close trend (${cadence})`));
12737
12941
  console.log();
12738
12942
  for (const b of recent) {
12739
- const newStr = b.new_arr > 0 ? chalk10.dim(` new $${formatShort(b.new_arr)}`) : "";
12740
- const expStr = b.expansion_arr > 0 ? chalk10.dim(` exp $${formatShort(b.expansion_arr)}`) : "";
12943
+ const newStr = b.new_arr > 0 ? chalk11.dim(` new $${formatShort(b.new_arr)}`) : "";
12944
+ const expStr = b.expansion_arr > 0 ? chalk11.dim(` exp $${formatShort(b.expansion_arr)}`) : "";
12741
12945
  console.log(
12742
- ` ${chalk10.dim(b.period.padEnd(8))} ${chalk10.bold("$" + formatShort(b.closed_won_total))}${newStr}${expStr} ${chalk10.dim(`(${b.closed_won_count} deals)`)}`
12946
+ ` ${chalk11.dim(b.period.padEnd(8))} ${chalk11.bold("$" + formatShort(b.closed_won_total))}${newStr}${expStr} ${chalk11.dim(`(${b.closed_won_count} deals)`)}`
12743
12947
  );
12744
12948
  }
12745
12949
  console.log();
@@ -12765,6 +12969,7 @@ var init_metrics_report = __esm({
12765
12969
  init_terminal();
12766
12970
  init_theme();
12767
12971
  init_companion();
12972
+ init_slides();
12768
12973
  GROUP_ORDER = [
12769
12974
  "Revenue",
12770
12975
  "Retention",
@@ -13299,7 +13504,7 @@ var diagnose_exports = {};
13299
13504
  __export(diagnose_exports, {
13300
13505
  handler: () => handler
13301
13506
  });
13302
- import chalk11 from "chalk";
13507
+ import chalk12 from "chalk";
13303
13508
  async function handler(args, ctx) {
13304
13509
  await hydrateAnalysisFromPersistedState(ctx);
13305
13510
  const { flags } = parseArgs(args, ["findings", "deep", "compact"]);
@@ -13328,9 +13533,9 @@ async function handler(args, ctx) {
13328
13533
  }
13329
13534
  if (options.findings && !canUseReplAi(ctx)) {
13330
13535
  console.log();
13331
- console.log(" " + chalk11.red("AI findings run only in the interactive REPL."));
13332
- console.log(" " + chalk11.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
13333
- console.log(" " + chalk11.dim("Start with ") + paint("accent", "ntrp") + chalk11.dim(", run ") + paint("accent", "/connect") + chalk11.dim(" (any provider key), then /diagnose --findings."));
13536
+ console.log(" " + chalk12.red("AI findings run only in the interactive REPL."));
13537
+ console.log(" " + chalk12.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
13538
+ console.log(" " + chalk12.dim("Start with ") + paint("accent", "ntrp") + chalk12.dim(", run ") + paint("accent", "/connect") + chalk12.dim(" (any provider key), then /diagnose --findings."));
13334
13539
  console.log();
13335
13540
  return;
13336
13541
  }
@@ -13362,7 +13567,7 @@ async function handler(args, ctx) {
13362
13567
  }
13363
13568
  ctx.skipTimeBankDiagnoseCredit = false;
13364
13569
  if (ctx.oneShot && options.findings) {
13365
- console.log(chalk11.dim(" For follow-up questions, run `ntrp` and ask in plain English."));
13570
+ console.log(chalk12.dim(" For follow-up questions, run `ntrp` and ask in plain English."));
13366
13571
  console.log();
13367
13572
  }
13368
13573
  return summary;
@@ -13420,7 +13625,7 @@ async function runDiagnose(options, ctx) {
13420
13625
  });
13421
13626
  return buildDiagnoseSummary(fullResult.aggregate, findings);
13422
13627
  } catch (err) {
13423
- console.error(chalk11.red(String(err)));
13628
+ console.error(chalk12.red(String(err)));
13424
13629
  process.exit(1);
13425
13630
  }
13426
13631
  }
@@ -13432,7 +13637,7 @@ async function runSegmentDiagnose(options) {
13432
13637
  spinner.succeed("Diagnosis complete");
13433
13638
  } catch (err) {
13434
13639
  spinner.fail("Diagnosis failed");
13435
- console.error(chalk11.red(String(err)));
13640
+ console.error(chalk12.red(String(err)));
13436
13641
  process.exit(1);
13437
13642
  }
13438
13643
  const needle = options.segment.toLowerCase();
@@ -13441,19 +13646,19 @@ async function runSegmentDiagnose(options) {
13441
13646
  const subs = result.segments.filter((s) => s.segment.name.toLowerCase().includes(needle));
13442
13647
  if (subs.length === 1) match = subs[0];
13443
13648
  else if (subs.length > 1) {
13444
- console.error(chalk11.yellow(`
13649
+ console.error(chalk12.yellow(`
13445
13650
  "${options.segment}" matches multiple segments:`));
13446
- for (const s of subs) console.log(chalk11.dim(` - ${s.segment.name}`));
13651
+ for (const s of subs) console.log(chalk12.dim(` - ${s.segment.name}`));
13447
13652
  console.log();
13448
13653
  return;
13449
13654
  }
13450
13655
  }
13451
13656
  if (!match) {
13452
- console.error(chalk11.red(`
13657
+ console.error(chalk12.red(`
13453
13658
  No segment matching "${options.segment}".`));
13454
13659
  if (result.segments.length > 0) {
13455
- console.log(chalk11.dim(" Available segments:"));
13456
- for (const s of result.segments) console.log(chalk11.dim(` - ${s.segment.name}`));
13660
+ console.log(chalk12.dim(" Available segments:"));
13661
+ for (const s of result.segments) console.log(chalk12.dim(` - ${s.segment.name}`));
13457
13662
  }
13458
13663
  console.log();
13459
13664
  return;
@@ -13507,161 +13712,1105 @@ var init_diagnose = __esm({
13507
13712
  }
13508
13713
  });
13509
13714
 
13510
- // src/services/session-analysis.ts
13511
- var session_analysis_exports = {};
13512
- __export(session_analysis_exports, {
13513
- buildExploreContextBlock: () => buildExploreContextBlock,
13514
- buildHandoffContextBlock: () => buildHandoffContextBlock,
13515
- formatAnalysisMissingError: () => formatAnalysisMissingError,
13516
- handoffInstructionPrefix: () => handoffInstructionPrefix,
13517
- hasAnyAnalysis: () => hasAnyAnalysis,
13518
- loadSessionAnalysisBundle: () => loadSessionAnalysisBundle
13519
- });
13520
- async function loadSessionAnalysisBundle() {
13521
- const [diagnosis, metrics] = await Promise.all([
13522
- loadLatestDiagnosis(),
13523
- loadLatestMetricsAnalysis()
13524
- ]);
13525
- return { diagnosis, metrics };
13526
- }
13527
- function hasAnyAnalysis(bundle) {
13528
- return bundle.diagnosis != null || bundle.metrics != null;
13529
- }
13530
- function formatAnalysisMissingError(ctx) {
13531
- const primary = ctx.analysis.primary;
13532
- if (primary === "revenue_metrics") {
13533
- return `No analysis found. Run ${paint("accent", "/new")} or ${paint("accent", "/metrics")} first.`;
13715
+ // src/services/session-analysis.ts
13716
+ var session_analysis_exports = {};
13717
+ __export(session_analysis_exports, {
13718
+ buildExploreContextBlock: () => buildExploreContextBlock,
13719
+ buildHandoffContextBlock: () => buildHandoffContextBlock,
13720
+ formatAnalysisMissingError: () => formatAnalysisMissingError,
13721
+ handoffInstructionPrefix: () => handoffInstructionPrefix,
13722
+ hasAnyAnalysis: () => hasAnyAnalysis,
13723
+ loadSessionAnalysisBundle: () => loadSessionAnalysisBundle
13724
+ });
13725
+ async function loadSessionAnalysisBundle() {
13726
+ const [diagnosis, metrics] = await Promise.all([
13727
+ loadLatestDiagnosis(),
13728
+ loadLatestMetricsAnalysis()
13729
+ ]);
13730
+ return { diagnosis, metrics };
13731
+ }
13732
+ function hasAnyAnalysis(bundle) {
13733
+ return bundle.diagnosis != null || bundle.metrics != null;
13734
+ }
13735
+ function formatAnalysisMissingError(ctx) {
13736
+ const primary = ctx.analysis.primary;
13737
+ if (primary === "revenue_metrics") {
13738
+ return `No analysis found. Run ${paint("accent", "/new")} or ${paint("accent", "/metrics")} first.`;
13739
+ }
13740
+ return `No analysis found. Run ${paint("accent", "/new")} or ${paint("accent", "/diagnose")} first.`;
13741
+ }
13742
+ function formatMetricLine(row) {
13743
+ const label = row.label ?? row.metric;
13744
+ const formatted = row.formatted ?? "--";
13745
+ const conf = row.confidence;
13746
+ const confStr = conf != null && conf < 80 ? ` (${conf}% conf)` : "";
13747
+ return `- ${label}: ${formatted}${confStr}`;
13748
+ }
13749
+ function buildHandoffContextBlock(bundle, ctx) {
13750
+ const { diagnosis, metrics } = bundle;
13751
+ const profile = loadProfile();
13752
+ const lines = [];
13753
+ if (profile?.company_name) {
13754
+ lines.push(`Company: ${profile.company_name} (${profile.industry})`);
13755
+ lines.push(`Sales motion: ${profile.sales_motion}${profile.average_deal_size ? ` \xB7 avg deal ${profile.average_deal_size}` : ""}`);
13756
+ if (profile.user_scope) lines.push(`My scope: ${profile.user_scope}`);
13757
+ }
13758
+ if (ctx.dataset?.label) {
13759
+ const counts = ctx.dataset.counts ?? {};
13760
+ const countStr = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k}`).join(", ");
13761
+ lines.push(`Dataset: ${ctx.dataset.label}${countStr ? ` (${countStr})` : ""}`);
13762
+ }
13763
+ const completed = ctx.analysis.completed;
13764
+ if (completed.length > 0) {
13765
+ lines.push(`Analysis lenses completed: ${completed.join(", ")}`);
13766
+ }
13767
+ lines.push("");
13768
+ if (diagnosis) {
13769
+ const { health, findings } = diagnosis;
13770
+ lines.push("## GTM health (vital signs)");
13771
+ lines.push(`Overall score: ${Math.round(health.overall_score)} (${health.overall_status})`);
13772
+ if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
13773
+ lines.push(`Total value at risk: ${formatCurrency(health.total_value_at_risk)}`);
13774
+ }
13775
+ lines.push("");
13776
+ lines.push("### Vital signs");
13777
+ for (const vs of health.vital_signs) {
13778
+ const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
13779
+ const dollars = vs.dollar_value != null ? ` \u2014 ${formatCurrency(vs.dollar_value)}${vs.dollar_label ? ` ${vs.dollar_label}` : ""}` : "";
13780
+ lines.push(`- ${label}: ${Math.round(vs.score)} (${vs.status})${dollars}`);
13781
+ }
13782
+ if (findings.length > 0) {
13783
+ lines.push("");
13784
+ lines.push("### GTM findings");
13785
+ appendFindings(lines, findings);
13786
+ }
13787
+ lines.push("");
13788
+ }
13789
+ if (metrics && metrics.metrics.length > 0) {
13790
+ lines.push("## SaaS metrics");
13791
+ const byKey = new Map(metrics.metrics.map((r) => [r.metric, r]));
13792
+ for (const key of KEY_METRICS) {
13793
+ const row = byKey.get(key);
13794
+ if (row) lines.push(formatMetricLine(row));
13795
+ }
13796
+ if (metrics.findings.length > 0) {
13797
+ lines.push("");
13798
+ lines.push("### Metrics findings");
13799
+ appendFindings(lines, metrics.findings);
13800
+ }
13801
+ lines.push("");
13802
+ }
13803
+ if (!diagnosis && metrics) {
13804
+ lines.unshift("Primary analysis: SaaS metrics (no GTM health snapshot on this session yet).", "");
13805
+ } else if (diagnosis && !metrics) {
13806
+ lines.push("(SaaS metrics not run on this session \u2014 run /metrics for the revenue view)");
13807
+ }
13808
+ return lines.join("\n").trim();
13809
+ }
13810
+ function buildExploreContextBlock(bundle, ctx) {
13811
+ const full = buildHandoffContextBlock(bundle, ctx);
13812
+ if (!full) return "";
13813
+ const lines = full.split("\n");
13814
+ const out = [
13815
+ "COMPLETED ANALYSIS (the user already saw the full report \u2014 cite this, do not re-dump it):",
13816
+ ""
13817
+ ];
13818
+ let inFindings = false;
13819
+ let findingCount = 0;
13820
+ for (const line of lines) {
13821
+ if (line.startsWith("### GTM findings") || line.startsWith("### Metrics findings")) {
13822
+ inFindings = true;
13823
+ out.push(line);
13824
+ continue;
13825
+ }
13826
+ if (inFindings && line.startsWith("- [")) {
13827
+ if (findingCount >= 5) continue;
13828
+ out.push(line);
13829
+ findingCount++;
13830
+ continue;
13831
+ }
13832
+ if (inFindings && line.startsWith("##")) {
13833
+ inFindings = false;
13834
+ }
13835
+ if (line.startsWith("## ") || line.startsWith("### Vital") || line.startsWith("- ") && !inFindings) {
13836
+ if (line.startsWith("(SaaS metrics not run")) continue;
13837
+ out.push(line);
13838
+ }
13839
+ if (line.startsWith("Overall score:") || line.startsWith("Total value at risk:")) {
13840
+ out.push(line);
13841
+ }
13842
+ if (line.startsWith("- ARR:") || line.startsWith("- NRR:") || line.startsWith("- GRR:")) {
13843
+ out.push(line);
13844
+ }
13845
+ }
13846
+ return out.join("\n").trim();
13847
+ }
13848
+ function appendFindings(lines, findings) {
13849
+ for (const f of findings.slice(0, 12)) {
13850
+ const dollars = f.dollar_value != null ? ` (${formatCurrency(f.dollar_value)})` : "";
13851
+ const plays = f.recommended_plays?.length ? ` \u2192 Plays: ${f.recommended_plays.map((p) => p.play_name).join(", ")}` : "";
13852
+ lines.push(`- [${f.severity}]${dollars} ${f.finding}${plays}`);
13853
+ }
13854
+ }
13855
+ function handoffInstructionPrefix(primary) {
13856
+ if (primary === "revenue_metrics") {
13857
+ return "the SaaS metrics and pipeline context below";
13858
+ }
13859
+ return "the pipeline diagnosis below";
13860
+ }
13861
+ var KEY_METRICS;
13862
+ var init_session_analysis = __esm({
13863
+ "src/services/session-analysis.ts"() {
13864
+ "use strict";
13865
+ init_queries();
13866
+ init_profile();
13867
+ init_formatters();
13868
+ init_theme();
13869
+ KEY_METRICS = ["arr", "nrr", "grr", "win_rate", "pipeline_coverage"];
13870
+ }
13871
+ });
13872
+
13873
+ // src/data/metric-definitions.ts
13874
+ function pctBand(metric, motion) {
13875
+ const m = motion ?? "mid_market";
13876
+ const t = METRICS_BENCHMARKS[m][metric];
13877
+ return `${motionBenchmarkLabel(m)} green \u2265${t.green}${metric === "pipeline_coverage" ? "x" : "%"}, yellow \u2265${t.yellow}${metric === "pipeline_coverage" ? "x" : "%"}`;
13878
+ }
13879
+ function monthsBand(motion) {
13880
+ const m = motion ?? "mid_market";
13881
+ const t = METRICS_BENCHMARKS[m].payback_months;
13882
+ return `${motionBenchmarkLabel(m)} green \u2264${t.green}mo, yellow \u2264${t.yellow}mo`;
13883
+ }
13884
+ function magicBand(motion) {
13885
+ const m = motion ?? "mid_market";
13886
+ const t = METRICS_BENCHMARKS[m].magic_number;
13887
+ return `${motionBenchmarkLabel(m)} green \u2265${t.green}, yellow \u2265${t.yellow}`;
13888
+ }
13889
+ function getMetricExplainer(id) {
13890
+ return BY_ID.get(id);
13891
+ }
13892
+ function resolveMetricId(query) {
13893
+ const q = query.trim().toLowerCase().replace(/\s+/g, " ");
13894
+ if (!q) return void 0;
13895
+ if (BY_ID.has(q)) return q;
13896
+ const direct = ALIAS_INDEX.get(q);
13897
+ if (direct) return direct;
13898
+ const norm = q.replace(/[-\s]+/g, "_");
13899
+ if (BY_ID.has(norm)) return norm;
13900
+ return ALIAS_INDEX.get(norm);
13901
+ }
13902
+ var VITALS, SAAS, METRIC_DEFINITIONS, BY_ID, ALIAS_INDEX, SAAS_METRIC_IDS;
13903
+ var init_metric_definitions = __esm({
13904
+ "src/data/metric-definitions.ts"() {
13905
+ "use strict";
13906
+ init_metrics_benchmarks();
13907
+ VITALS = [
13908
+ {
13909
+ id: "freshness",
13910
+ kind: "vital",
13911
+ label: "Freshness",
13912
+ group: "Vital Signs",
13913
+ tagline: "Is your CRM telling the truth about what's alive?",
13914
+ how_computed: "Weighted average of people, organizations, and opportunities with recent activity (and open opps not past-due). Defaults: people/orgs 90-day window, opps 30-day window; weights 35/30/35.",
13915
+ formula_lines: [
13916
+ "freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
13917
+ "people/orgs fresh if activity within 90d",
13918
+ "opps fresh if activity within 30d AND not past-due"
13919
+ ],
13920
+ meaning: 'Board question: "how much of this pipeline is real vs fiction?" Dollar value = sum of amount on stale opportunities \u2014 pipeline at risk.',
13921
+ expert_read: "Cut by owner and by stage first \u2014 freshness reds concentrate on people or process, rarely evenly. In a long-cycle enterprise motion 30 quiet days can be normal cadence; in a velocity motion it's a dead deal. A sudden cliff usually means a broken integration or a departed rep, not gradual decay. False positive to check: bulk-imported records nobody has touched yet.",
13922
+ deepdive: [
13923
+ "Status: green \u226580, yellow \u226560, red below 60 (motion presets can shift windows).",
13924
+ 'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
13925
+ "Layer 1 of the gating stack \u2014 a red here bounds what you can trust downstream.",
13926
+ "Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when score < 60.",
13927
+ "Levers: stale-deal alert at N quiet days, weekly hygiene scrub, enrichment refresh on quiet records, signal-triggered reactivation for paid-for dormant accounts."
13928
+ ],
13929
+ visual: {
13930
+ kind: "bars",
13931
+ caption: "Exemplar component mix (higher = fresher)",
13932
+ bars: [
13933
+ { label: "People", value: 72, tone: "yellow" },
13934
+ { label: "Organizations", value: 81, tone: "green" },
13935
+ { label: "Opportunities", value: 44, tone: "red" }
13936
+ ]
13937
+ },
13938
+ play_id: "clean-dead-pipeline",
13939
+ dollar_label: "pipeline at risk",
13940
+ audience: {
13941
+ board: "Freshness answers whether the pipeline number is real. Low freshness means forecast risk \u2014 stale deals inflate coverage and hide the true gap.",
13942
+ ops: "Score = weighted recency across people/orgs/opps. Cut by owner and stage; install a stale-deal alert and weekly scrub. Play: Clean Dead Pipeline."
13943
+ },
13944
+ aliases: ["data freshness", "stale", "zombie deals", "crm freshness"]
13945
+ },
13946
+ {
13947
+ id: "flow_rate",
13948
+ kind: "vital",
13949
+ label: "Flow Rate",
13950
+ group: "Vital Signs",
13951
+ tagline: "How fast do deals actually move \u2014 and where do they die?",
13952
+ how_computed: "Base score from average open-deal age vs max_days, then a penalty (up to \u221220) for the share of stuck deals (no update beyond stuck_days, or past-due close). Status is driven by average open age, not the score alone.",
13953
+ formula_lines: [
13954
+ "base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
13955
+ "score = base \u2212 stuckSharePenalty (\u226420)",
13956
+ "stuck = no update > stuck_days OR past-due close"
13957
+ ],
13958
+ meaning: 'Board question: "is next quarter slipping because deals are stuck?" Dollar value = amount stuck in pipeline.',
13959
+ expert_read: "Cut by stage-age, not just deal-age \u2014 find the stage where deals go to die (usually one). Compare stuck-deal age to this company's own median cycle, not a generic norm. Stuck + past-due close dates together signal happy-ears forecasting, a credibility problem before it's a revenue problem.",
13960
+ deepdive: [
13961
+ "Status from avg open age: \u226445d green, \u226490d yellow, else red (defaults; max_days 120, stuck_days 60).",
13962
+ 'Dollar translation: sum of amount on stuck deals \u2192 "stuck in pipeline".',
13963
+ "Layer 2 of the gating stack (with Drop Rate).",
13964
+ "Trigger play: Unstick the Pipeline (unstick-pipeline) when score is weak.",
13965
+ "Levers: stage-age report, past-due close cleanup, progression plans on stuck deals, forecast hygiene on happy-ears dates."
13966
+ ],
13967
+ visual: {
13968
+ kind: "funnel",
13969
+ caption: "Exemplar stage ages \u2014 find the stage where deals go to die",
13970
+ funnel: [
13971
+ { label: "Discovery", widthPct: 100 },
13972
+ { label: "Qualify", widthPct: 78 },
13973
+ { label: "Propose", widthPct: 55 },
13974
+ { label: "Negotiate", widthPct: 22 },
13975
+ { label: "Closed", widthPct: 12 }
13976
+ ]
13977
+ },
13978
+ play_id: "unstick-pipeline",
13979
+ dollar_label: "stuck in pipeline",
13980
+ audience: {
13981
+ board: "Flow Rate is velocity risk. Stuck pipeline with past-due closes is a credibility problem for the forecast before it is a revenue miss.",
13982
+ ops: "Find the stage with collapsing advancement and age. Clear past-due closes, write progression plans on stuck deals. Play: Unstick the Pipeline."
13983
+ },
13984
+ aliases: ["flow rate", "deal velocity", "stuck deals", "stuck pipeline"]
13985
+ },
13986
+ {
13987
+ id: "drop_rate",
13988
+ kind: "vital",
13989
+ label: "Drop Rate",
13990
+ group: "Vital Signs",
13991
+ tagline: "Where do leads vanish between systems?",
13992
+ how_computed: "Blend of cross-system retention (marketing people also present in sales) and opportunity retention (open opps not abandoned). Defaults weight cross-system 60% / opp retention 40%. Abandoned = open opps with no activity in 30 days.",
13993
+ formula_lines: [
13994
+ "score = crossSystemRetention\xD70.6 + oppRetention\xD70.4",
13995
+ "cross-system = marketing people also in sales CRM",
13996
+ "abandoned = open opps with no activity in 30d"
13997
+ ],
13998
+ meaning: 'Board question: "how much pipeline are we paying for and never working?" Dollar value = droppedCount \xD7 conversionRate \xD7 avgDealSize \u2014 est. lost at handoff.',
13999
+ expert_read: "This is almost always a systems failure \u2014 routing rules, unassigned territories, dead rep queues, or a sync gap between marketing and CRM \u2014 not lazy reps. First cut by lead source; the leak usually concentrates in one or two sources. The cheapest pipeline this business can buy is the leads it already paid for.",
14000
+ deepdive: [
14001
+ "Status: green \u226580, yellow \u226560, red below 60.",
14002
+ 'Dollar translation: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
14003
+ "Layer 2 of the gating stack (with Flow Rate).",
14004
+ "Trigger play: Fix the Handoff Gap (fix-handoff-gap) when drop is high.",
14005
+ "Levers: source-level handoff audit, routing + sync repair, time-to-first-touch SLA, weekly marketing-only-leads report."
14006
+ ],
14007
+ visual: {
14008
+ kind: "funnel",
14009
+ caption: "Exemplar handoff funnel \u2014 the leak is usually one or two sources",
14010
+ funnel: [
14011
+ { label: "Marketing leads", widthPct: 100 },
14012
+ { label: "In sales CRM", widthPct: 62 },
14013
+ { label: "Assigned + touched", widthPct: 41 },
14014
+ { label: "Active opportunities", widthPct: 28 }
14015
+ ]
14016
+ },
14017
+ play_id: "fix-handoff-gap",
14018
+ dollar_label: "est. lost at handoff",
14019
+ audience: {
14020
+ board: "Drop Rate prices the handoff leak \u2014 budget already spent on leads that never reach a working rep. Usually a systems failure, not a people failure.",
14021
+ ops: "Audit by source, fix routing/sync/dead queues, instrument time-to-first-touch. Play: Fix the Handoff Gap."
14022
+ },
14023
+ aliases: ["drop rate", "handoff", "handoff gap", "lead leak", "marketing sales handoff"]
14024
+ },
14025
+ {
14026
+ id: "signal_to_noise",
14027
+ kind: "vital",
14028
+ label: "Signal:Noise",
14029
+ group: "Vital Signs",
14030
+ tagline: "How much activity is aimed at deals that can still close?",
14031
+ how_computed: "Over a 90-day lookback: (signal activities / all activities) \xD7 100. Signal = activity linked to an open opportunity, a pipeline person, or a pipeline organization.",
14032
+ formula_lines: [
14033
+ "score = (signalCount / activityCount) \xD7 100",
14034
+ "signal = linked to open opp / pipeline person / pipeline org",
14035
+ "lookback = trailing 90 days"
14036
+ ],
14037
+ meaning: 'Board question: "are we burning capacity on dead water?" Dollar value = noiseCount \xD7 hours_per_activity \xD7 rep_hourly_cost \u2014 misdirected effort.',
14038
+ expert_read: "Cut by rep and by account status \u2014 noise usually means reps fishing in the pond they can see (dead accounts they know) because targeting and account lists are stale. Persistent noise is a coverage-model problem, not a coaching problem. Check whether activity is logged against closed or unlinked records \u2014 often a hygiene artifact.",
14039
+ deepdive: [
14040
+ "Status: green \u226565, yellow \u226540, red below 40.",
14041
+ "Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost).",
14042
+ "Layer 3 of the gating stack \u2014 trust Freshness / Flow / Drop before reading activity efficiency.",
14043
+ "Trigger play: Retarget Misdirected Effort (retarget-effort) when score is low.",
14044
+ "Levers: refresh account lists, signal-based targeting, stop logging against closed/unlinked records, coverage-model redesign."
14045
+ ],
14046
+ visual: {
14047
+ kind: "split",
14048
+ caption: "Exemplar activity mix \u2014 signal vs noise",
14049
+ bars: [
14050
+ { label: "Signal", value: 38, tone: "green" },
14051
+ { label: "Noise", value: 62, tone: "red" }
14052
+ ]
14053
+ },
14054
+ play_id: "retarget-effort",
14055
+ dollar_label: "misdirected effort",
14056
+ audience: {
14057
+ board: "Signal:Noise prices wasted capacity. Persistent noise is usually a coverage-model problem, not a coaching problem \u2014 reps fish in dead ponds they already know.",
14058
+ ops: "Score = % of activities linked to live pipeline. Cut by rep and account status; refresh targeting. Play: Retarget Misdirected Effort."
14059
+ },
14060
+ aliases: ["signal to noise", "signal:noise", "s/n", "activity efficiency", "noise"]
14061
+ },
14062
+ {
14063
+ id: "thread_depth",
14064
+ kind: "vital",
14065
+ label: "Thread Depth",
14066
+ group: "Vital Signs",
14067
+ tagline: "How fragile is the pipeline if one champion goes dark?",
14068
+ how_computed: "Percent of open deals with at least multi_thread_threshold (default 2) distinct people active in the last 90 days (opp-direct contacts + same-org activity).",
14069
+ formula_lines: [
14070
+ "score = % open deals with \u22652 active people (90d)",
14071
+ "people counted via opp contacts + same-org activity",
14072
+ "threshold configurable (default 2)"
14073
+ ],
14074
+ meaning: 'Board question: "how much revenue dies if one contact changes jobs?" Dollar value = sum of amount on single-threaded deals.',
14075
+ expert_read: "Weight by deal size \u2014 one single-threaded mega-deal outweighs ten small ones. Single-threading late in the cycle is far more dangerous than early. In enterprise motions, thread depth is a leading indicator of slipped quarters: champions change jobs, and there's no second door in.",
14076
+ deepdive: [
14077
+ "Status: green \u226565, yellow \u226540, red below 40.",
14078
+ 'Dollar translation: sum of amount on single-threaded deals \u2192 "single-threaded".',
14079
+ "Layer 4 of the gating stack \u2014 read last, after the upstream vitals.",
14080
+ "Trigger play: Multi-Thread Your Deals (multi-thread-deals) when depth is low.",
14081
+ "Levers: buying-committee map, warm internal referral first, CRM contact roles, mid-stage single-thread alerts, champion job-change signals."
14082
+ ],
14083
+ visual: {
14084
+ kind: "bars",
14085
+ caption: "Exemplar \u2014 multi-threaded vs single-threaded open deals",
14086
+ bars: [
14087
+ { label: "Multi-threaded", value: 34, tone: "green" },
14088
+ { label: "Single-threaded", value: 66, tone: "red" }
14089
+ ]
14090
+ },
14091
+ play_id: "multi-thread-deals",
14092
+ dollar_label: "single-threaded",
14093
+ audience: {
14094
+ board: "Thread Depth is resilience risk. One single-threaded mega-deal outweighs ten small ones \u2014 late-cycle single-threading is a leading indicator of slipped quarters.",
14095
+ ops: "Score = % of open deals with \u22652 active contacts in 90d. Map the buying committee; alert on mid-stage singles. Play: Multi-Thread Your Deals."
14096
+ },
14097
+ aliases: ["thread depth", "multithreading", "multi-thread", "single-threaded", "buying committee"]
14098
+ }
14099
+ ];
14100
+ SAAS = [
14101
+ // —— Revenue ——
14102
+ {
14103
+ id: "arr",
14104
+ kind: "saas",
14105
+ label: "ARR",
14106
+ group: "Revenue",
14107
+ tagline: "How big is the revenue engine \u2014 and from where?",
14108
+ how_computed: "Sum of amount on closed-won opportunities in the dataset (pipeline-inferred ARR when a pure subscription ledger is unavailable).",
14109
+ formula_lines: [
14110
+ "ARR \u2248 \u03A3 amount on closed-won opportunities",
14111
+ "New + Expansion = growth \xB7 Churned + Contraction = leakage"
14112
+ ],
14113
+ meaning: 'Board question: "how fast are we growing, and from where?" Always decompose growth into new vs expansion \u2014 the mix is the story.',
14114
+ expert_read: "Always decompose growth into new vs expansion \u2014 the mix is the story. Instrument trust: prefer this company's own trailing history over any external prior; a number below its reliability gate is a hypothesis, not a fact.",
14115
+ deepdive: [
14116
+ "Companion metrics: New ARR, Expansion ARR, Churned ARR, Contraction ARR.",
14117
+ "Estimation method may be ledger, pipeline_inferred, or snapshot \u2014 read confidence + reliability_gate.",
14118
+ "Cross-check with Freshness before trusting ARR growth stories built on zombie deals."
14119
+ ],
14120
+ visual: {
14121
+ kind: "waterfall",
14122
+ caption: "Exemplar ARR walk \u2014 growth vs leakage",
14123
+ waterfall: [
14124
+ { label: "Starting", delta: 100, cumulative: 100 },
14125
+ { label: "+ New", delta: 18, cumulative: 118 },
14126
+ { label: "+ Expansion", delta: 12, cumulative: 130 },
14127
+ { label: "\u2212 Contraction", delta: -4, cumulative: 126 },
14128
+ { label: "\u2212 Churned", delta: -8, cumulative: 118 }
14129
+ ]
14130
+ },
14131
+ audience: {
14132
+ board: "ARR is the size of the engine. The story is the mix \u2014 new vs expansion growth, and how much leakage (churn + contraction) ate it.",
14133
+ ops: "Computed as \u03A3 closed-won amounts (pipeline-inferred when no ledger). Decompose into new / expansion / churned / contraction before briefing anyone."
14134
+ },
14135
+ aliases: ["annual recurring revenue", "revenue"]
14136
+ },
14137
+ {
14138
+ id: "new_arr",
14139
+ kind: "saas",
14140
+ label: "New ARR",
14141
+ group: "Revenue",
14142
+ tagline: "How much growth came from brand-new customers?",
14143
+ how_computed: "Closed-won tagged New Business, or first closed-won deal per organization when tags are missing.",
14144
+ formula_lines: [
14145
+ "New ARR = \u03A3 closed-won tagged New Business",
14146
+ "fallback: first closed-won deal per organization"
14147
+ ],
14148
+ meaning: 'Board question: "is growth coming from the top of funnel, or are we farming the base?"',
14149
+ expert_read: "Rising New ARR with falling Expansion usually means land-and-expand is underpowered \u2014 packaging or CS motion, not just sales capacity.",
14150
+ deepdive: [
14151
+ "Pair with Expansion ARR \u2014 the mix tells you which motion is carrying growth.",
14152
+ "Tag quality matters: untagged deals fall into the first-deal-per-org heuristic."
14153
+ ],
14154
+ visual: {
14155
+ kind: "bars",
14156
+ caption: "Exemplar growth mix",
14157
+ bars: [
14158
+ { label: "New ARR", value: 60, tone: "accent" },
14159
+ { label: "Expansion ARR", value: 40, tone: "green" }
14160
+ ]
14161
+ },
14162
+ audience: {
14163
+ board: "New ARR is net-new logos. Read it next to Expansion \u2014 a healthy mix beats a one-sided engine.",
14164
+ ops: "Prefer CRM New Business tags; otherwise first closed-won per org. Watch tag hygiene."
14165
+ },
14166
+ aliases: ["new business arr", "new logo arr"]
14167
+ },
14168
+ {
14169
+ id: "expansion_arr",
14170
+ kind: "saas",
14171
+ label: "Expansion ARR",
14172
+ group: "Revenue",
14173
+ tagline: "How much are existing customers buying more?",
14174
+ how_computed: "Closed-won tagged Expansion, or later closed-won deals per organization after the first win.",
14175
+ formula_lines: [
14176
+ "Expansion ARR = \u03A3 closed-won tagged Expansion",
14177
+ "fallback: later closed-won deals per organization"
14178
+ ],
14179
+ meaning: 'Board question: "is the installed base compounding?"',
14180
+ expert_read: "Expansion is the cheapest growth. Weak Expansion with strong New ARR is a land-only motion \u2014 packaging, CS capacity, or product attach is usually the lever.",
14181
+ deepdive: [
14182
+ "Feeds NRR as the upside term.",
14183
+ "Compare to Contraction \u2014 net expansion = expansion \u2212 contraction."
14184
+ ],
14185
+ visual: {
14186
+ kind: "bars",
14187
+ caption: "Exemplar \u2014 expansion vs contraction",
14188
+ bars: [
14189
+ { label: "Expansion", value: 70, tone: "green" },
14190
+ { label: "Contraction", value: 25, tone: "yellow" }
14191
+ ]
14192
+ },
14193
+ audience: {
14194
+ board: "Expansion ARR is installed-base compounding \u2014 the cheapest growth when it works.",
14195
+ ops: "Tagged Expansion or subsequent wins per org. Pair with Contraction before celebrating net expansion."
14196
+ },
14197
+ aliases: ["upsell", "upsell arr", "cross-sell"]
14198
+ },
14199
+ {
14200
+ id: "churned_arr",
14201
+ kind: "saas",
14202
+ label: "Churned ARR",
14203
+ group: "Revenue",
14204
+ tagline: "How much revenue walked out the door?",
14205
+ how_computed: "Organizations with historical wins, no win in the trailing 12 months, and no active open opportunity \u2014 sum of their historical closed-won amounts.",
14206
+ formula_lines: [
14207
+ "Churned ARR = \u03A3 historical wins for orgs with",
14208
+ " no win in trailing 12mo AND no active open opp"
14209
+ ],
14210
+ meaning: `Board question: "how leaky is the bucket before expansion papers over it?" (with Contraction, this is GRR's downside).`,
14211
+ expert_read: "Pipeline-inferred churn is a hypothesis \u2014 confirm with billing status when available. A spike often clusters in one segment or cohort.",
14212
+ deepdive: [
14213
+ "Feeds GRR and NRR as the churn term.",
14214
+ "Cut by segment / motion before treating it as a company-wide PMF problem."
14215
+ ],
14216
+ visual: {
14217
+ kind: "bars",
14218
+ caption: "Exemplar leakage mix",
14219
+ bars: [
14220
+ { label: "Churned", value: 55, tone: "red" },
14221
+ { label: "Contraction", value: 30, tone: "yellow" }
14222
+ ]
14223
+ },
14224
+ audience: {
14225
+ board: "Churned ARR is full logo loss. With Contraction it sets the floor of the business (GRR).",
14226
+ ops: "Heuristic: historical winners with no trailing-12 win and no open opp. Validate against billing when you can."
14227
+ },
14228
+ aliases: ["churn", "logo churn", "churned revenue"]
14229
+ },
14230
+ {
14231
+ id: "contraction_arr",
14232
+ kind: "saas",
14233
+ label: "Contraction ARR",
14234
+ group: "Revenue",
14235
+ tagline: "How much did existing customers buy less?",
14236
+ how_computed: "Organizations with \u22652 wins where the latest amount is less than the prior \u2014 sum of the negative deltas.",
14237
+ formula_lines: [
14238
+ "Contraction = \u03A3 (prior \u2212 latest) where latest < prior",
14239
+ "requires \u22652 closed-won deals per organization"
14240
+ ],
14241
+ meaning: 'Board question: "are we quietly shrinking inside the base while logos stay?"',
14242
+ expert_read: "Contraction is often packaging, seat-reduction, or downgrade \u2014 different owner than logo churn. Same NRR can be a churn problem or a no-expansion problem.",
14243
+ deepdive: [
14244
+ "Feeds GRR and NRR.",
14245
+ "Needs multi-deal history per org \u2014 thin history understates contraction."
14246
+ ],
14247
+ visual: {
14248
+ kind: "waterfall",
14249
+ caption: "Exemplar \u2014 contraction digs into the base",
14250
+ waterfall: [
14251
+ { label: "Prior", delta: 100, cumulative: 100 },
14252
+ { label: "Latest", delta: -18, cumulative: 82 }
14253
+ ]
14254
+ },
14255
+ audience: {
14256
+ board: "Contraction is silent shrink inside retained logos \u2014 often packaging or seats, not a cancelled contract.",
14257
+ ops: "Requires \u22652 wins per org with a down-round. Pair with Expansion for net expansion."
14258
+ },
14259
+ aliases: ["downgrade", "seat reduction", "contraction"]
14260
+ },
14261
+ // —— Retention ——
14262
+ {
14263
+ id: "nrr",
14264
+ kind: "saas",
14265
+ label: "Net Revenue Retention",
14266
+ group: "Retention",
14267
+ tagline: "Would this business grow if sales stopped selling?",
14268
+ how_computed: "startingArr = ARR + Churned + Contraction \u2212 Expansion; NRR = ((starting \u2212 Churned \u2212 Contraction + Expansion) / starting) \xD7 100.",
14269
+ formula_lines: [
14270
+ "starting = ARR + churned + contraction \u2212 expansion",
14271
+ "NRR = (starting \u2212 churned \u2212 contraction + expansion) / starting \xD7 100",
14272
+ "NRR = 100% + expansion% \u2212 contraction% \u2212 churn%"
14273
+ ],
14274
+ meaning: 'Board question: "would this business grow if sales stopped selling?" >100% means growing from existing customers.',
14275
+ expert_read: "Decompose before judging: the same 95% can be a churn problem (product/PMF) or a no-expansion problem (packaging/motion) with different owners. Priors by segment: ~97% SMB, ~108% mid-market, ~118% enterprise medians; 110%+ is a strong signal at any stage.",
14276
+ deepdive: [
14277
+ "Always show the waterfall: +expansion \u2212contraction \u2212churn.",
14278
+ "GRR is the floor; NRR adds expansion on top.",
14279
+ "On pipeline-only data, treat as a hypothesis \u2014 check confidence / reliability_gate."
14280
+ ],
14281
+ visual: {
14282
+ kind: "waterfall",
14283
+ caption: "Exemplar NRR walk from 100%",
14284
+ waterfall: [
14285
+ { label: "100%", delta: 100, cumulative: 100 },
14286
+ { label: "+ Expansion", delta: 14, cumulative: 114 },
14287
+ { label: "\u2212 Contraction", delta: -4, cumulative: 110 },
14288
+ { label: "\u2212 Churn", delta: -6, cumulative: 104 }
14289
+ ]
14290
+ },
14291
+ audience: {
14292
+ board: "NRR >100% means the base compounds without new logos. Decompose before judging \u2014 same number, different owners.",
14293
+ ops: "NRR = 100 + expansion \u2212 contraction \u2212 churn. Motion benchmarks calibrate green/yellow bands. Check reliability_gate on pipeline-inferred data."
14294
+ },
14295
+ aliases: ["net revenue retention", "net retention", "ndr"],
14296
+ benchmarkHint: (motion) => pctBand("nrr", motion)
14297
+ },
14298
+ {
14299
+ id: "grr",
14300
+ kind: "saas",
14301
+ label: "Gross Revenue Retention",
14302
+ group: "Retention",
14303
+ tagline: "How leaky is the bucket before expansion papers over it?",
14304
+ how_computed: "GRR = ((startingArr \u2212 Churned \u2212 Contraction) / startingArr) \xD7 100 \u2014 expansion is excluded on purpose.",
14305
+ formula_lines: [
14306
+ "starting = ARR + churned + contraction \u2212 expansion",
14307
+ "GRR = (starting \u2212 churned \u2212 contraction) / starting \xD7 100"
14308
+ ],
14309
+ meaning: 'Board question: "how leaky is the bucket before expansion papers over it?" Prior: >90% healthy, >95% strong for enterprise.',
14310
+ expert_read: "GRR is the honesty metric. Expansion can make NRR look fine while GRR is quietly eroding \u2014 always read both.",
14311
+ deepdive: [
14312
+ "GRR never includes Expansion \u2014 that is the point.",
14313
+ "Owners: product/CS for churn, packaging for contraction."
14314
+ ],
14315
+ visual: {
14316
+ kind: "gauge",
14317
+ caption: "Exemplar GRR \u2014 floor of the business",
14318
+ gauge: 92
14319
+ },
14320
+ audience: {
14321
+ board: "GRR is the floor \u2014 churn + contraction only. Expansion cannot paper over a leaky bucket here.",
14322
+ ops: "Exclude Expansion by design. Pair with NRR; diagnose churn vs contraction separately."
14323
+ },
14324
+ aliases: ["gross revenue retention", "gross retention"],
14325
+ benchmarkHint: (motion) => pctBand("grr", motion)
14326
+ },
14327
+ // —— Pipeline ——
14328
+ {
14329
+ id: "pipeline_coverage",
14330
+ kind: "saas",
14331
+ label: "Pipeline Coverage",
14332
+ group: "Pipeline",
14333
+ tagline: "Is next quarter already at risk?",
14334
+ how_computed: "Open pipeline amount \xF7 trailing-90-day closed-won amount.",
14335
+ formula_lines: [
14336
+ "Coverage = openPipeline / trailing_90d_won",
14337
+ "required \u2248 1 / win_rate (discount for time left)"
14338
+ ],
14339
+ meaning: 'Board question: "is next quarter already at risk?" Priors scale with cycle length: ~3x velocity/SMB, 4\u20135x enterprise.',
14340
+ expert_read: "Coverage means nothing without win rate: required coverage \u2248 1 / win rate, discounted for time left in period. Inflated stages and zombie deals fake coverage \u2014 cross-check with Freshness before trusting it.",
14341
+ deepdive: [
14342
+ "Always pair with Win Rate and Freshness.",
14343
+ "Weighted Pipeline is the credibility-adjusted cousin."
14344
+ ],
14345
+ visual: {
14346
+ kind: "gauge",
14347
+ caption: "Exemplar coverage vs a 3x target",
14348
+ gauge: 72,
14349
+ bars: [
14350
+ { label: "Open pipeline", value: 75, tone: "accent" },
14351
+ { label: "Trailing won (scaled)", value: 25, tone: "neutral" }
14352
+ ]
14353
+ },
14354
+ audience: {
14355
+ board: "Coverage answers whether next quarter is already under-piped. Fake coverage from zombies is worse than an honest gap.",
14356
+ ops: "open / trailing-90d won. Required \u2248 1/win_rate. Cross-check Freshness before briefing."
14357
+ },
14358
+ aliases: ["coverage", "pipeline coverage", "pipe coverage"],
14359
+ benchmarkHint: (motion) => pctBand("pipeline_coverage", motion)
14360
+ },
14361
+ {
14362
+ id: "weighted_pipeline",
14363
+ kind: "saas",
14364
+ label: "Weighted Pipeline",
14365
+ group: "Pipeline",
14366
+ tagline: "What is the pipeline worth after stage probability?",
14367
+ how_computed: "Sum of amount \xD7 stage probability for open deals (CRM Probability when present, else stage defaults).",
14368
+ formula_lines: [
14369
+ "Weighted = \u03A3 (amount \xD7 stageProbability)",
14370
+ "trust \u2264 stage discipline deserves"
14371
+ ],
14372
+ meaning: 'Board question: "what should we actually forecast from open pipe?"',
14373
+ expert_read: "Trust it only as much as stage discipline deserves. Inflated late stages make weighted pipeline a fiction.",
14374
+ deepdive: [
14375
+ "Compare to unweighted open pipeline \u2014 a huge gap means optimistic stages.",
14376
+ "Pair with Flow Rate (stuck late stages)."
14377
+ ],
14378
+ visual: {
14379
+ kind: "bars",
14380
+ caption: "Exemplar \u2014 open vs weighted",
14381
+ bars: [
14382
+ { label: "Open pipeline", value: 100, tone: "neutral" },
14383
+ { label: "Weighted", value: 42, tone: "accent" }
14384
+ ]
14385
+ },
14386
+ audience: {
14387
+ board: "Weighted Pipeline is the credibility-adjusted forecast input \u2014 only as good as stage discipline.",
14388
+ ops: "\u03A3 amount \xD7 probability. Audit stage probabilities when weighted << open."
14389
+ },
14390
+ aliases: ["weighted pipe", "probability-weighted pipeline"]
14391
+ },
14392
+ {
14393
+ id: "pipeline_created",
14394
+ kind: "saas",
14395
+ label: "Pipeline Created (90d)",
14396
+ group: "Pipeline",
14397
+ tagline: "How much new pipe did we generate recently?",
14398
+ how_computed: "Sum of amounts for opportunities created in the last 90 days.",
14399
+ formula_lines: ["Pipeline Created = \u03A3 amount where created_at within 90d"],
14400
+ meaning: 'Board question: "is the top of funnel still filling?"',
14401
+ expert_read: "Falling created pipeline with flat coverage is a future miss \u2014 coverage is lagging; created is leading.",
14402
+ deepdive: [
14403
+ "Leading indicator for next-quarter coverage.",
14404
+ "Cut by source / segment to find where creation stalled."
14405
+ ],
14406
+ visual: {
14407
+ kind: "bars",
14408
+ caption: "Exemplar \u2014 created vs needed",
14409
+ bars: [
14410
+ { label: "Created (90d)", value: 55, tone: "yellow" },
14411
+ { label: "Target pace", value: 80, tone: "green" }
14412
+ ]
14413
+ },
14414
+ audience: {
14415
+ board: "Pipeline Created is a leading indicator \u2014 coverage lagging means the miss is already in motion.",
14416
+ ops: "\u03A3 amounts on opps created in 90d. Cut by source when it dips."
14417
+ },
14418
+ aliases: ["pipe gen", "pipeline generation", "created pipeline"]
14419
+ },
14420
+ {
14421
+ id: "pipeline_velocity",
14422
+ kind: "saas",
14423
+ label: "Pipeline Velocity",
14424
+ group: "Pipeline",
14425
+ tagline: "Revenue throughput per day \u2014 four levers, one number.",
14426
+ how_computed: "(openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays \u2014 requires \u22653 dated closed-won deals. Unit: $/day.",
14427
+ formula_lines: [
14428
+ "Velocity = (openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays",
14429
+ "four levers: #opps \xB7 deal size \xB7 win rate \xB7 cycle days"
14430
+ ],
14431
+ meaning: 'Board question: "which lever moved when throughput changed?" The most decision-ready pipeline metric.',
14432
+ expert_read: "When velocity changes, name WHICH lever moved. A win-rate rise on falling opp volume is qualification tightening, not improvement.",
14433
+ deepdive: [
14434
+ "Needs \u22653 dated wins \u2014 otherwise unavailable.",
14435
+ "Pairs with Flow Rate (cycle) and Win Rate (conversion)."
14436
+ ],
14437
+ visual: {
14438
+ kind: "levers",
14439
+ caption: "Four levers \u2014 say which one moved",
14440
+ levers: ["# Open opps", "Avg deal size", "Win rate", "Cycle days"]
14441
+ },
14442
+ audience: {
14443
+ board: "Velocity is throughput. When it moves, demand the lever \u2014 volume, size, win rate, or cycle \u2014 not a shrug.",
14444
+ ops: "(opps \xD7 avgDeal \xD7 winRate) / cycleDays. Diagnose the moved lever before prescribing."
14445
+ },
14446
+ aliases: ["velocity", "pipeline velocity", "throughput"]
14447
+ },
14448
+ // —— Sales efficiency ——
14449
+ {
14450
+ id: "win_rate",
14451
+ kind: "saas",
14452
+ label: "Win Rate",
14453
+ group: "Sales Efficiency",
14454
+ tagline: "Of decided deals, how often do we win?",
14455
+ how_computed: "closed-won / (won + lost) \xD7 100.",
14456
+ formula_lines: ["Win Rate = won / (won + lost) \xD7 100"],
14457
+ meaning: 'Board question: "are we converting the pipe we create?" Priors: 25\u201335% SMB, 18\u201325% mid-market, 12\u201318% enterprise on qualified opps.',
14458
+ expert_read: "A rising win rate on falling opp volume is qualification tightening, not improvement \u2014 check the denominator.",
14459
+ deepdive: [
14460
+ "Required coverage \u2248 1 / win rate.",
14461
+ "Cut by segment / source before company-wide coaching."
14462
+ ],
14463
+ visual: {
14464
+ kind: "split",
14465
+ caption: "Exemplar decided deals",
14466
+ bars: [
14467
+ { label: "Won", value: 28, tone: "green" },
14468
+ { label: "Lost", value: 72, tone: "red" }
14469
+ ]
14470
+ },
14471
+ audience: {
14472
+ board: "Win Rate is conversion of decided deals. Rising win rate with falling volume is often tighter qualification, not better selling.",
14473
+ ops: "won/(won+lost). Check the denominator. Motion benchmarks set green/yellow bands."
14474
+ },
14475
+ aliases: ["close rate", "winrate", "win %"],
14476
+ benchmarkHint: (motion) => pctBand("win_rate", motion)
14477
+ },
14478
+ {
14479
+ id: "avg_deal_size",
14480
+ kind: "saas",
14481
+ label: "Avg Deal Size",
14482
+ group: "Sales Efficiency",
14483
+ tagline: "What does a typical win look like?",
14484
+ how_computed: "Mean amount on closed-won opportunities.",
14485
+ formula_lines: ["Avg Deal = mean(closed-won amount)"],
14486
+ meaning: 'Board question: "are we selling the motion we think we are?"',
14487
+ expert_read: "Deal size drifting down while volume rises often means mix shift into a lower segment \u2014 not always a problem, but it changes coverage math.",
14488
+ deepdive: [
14489
+ "Feeds Pipeline Velocity and LTV proxy.",
14490
+ "Cut by segment \u2014 averages hide bimodal motions."
14491
+ ],
14492
+ visual: {
14493
+ kind: "bars",
14494
+ caption: "Exemplar \u2014 size mix by segment",
14495
+ bars: [
14496
+ { label: "SMB", value: 30, tone: "neutral" },
14497
+ { label: "Mid-market", value: 55, tone: "accent" },
14498
+ { label: "Enterprise", value: 90, tone: "green" }
14499
+ ]
14500
+ },
14501
+ audience: {
14502
+ board: "Avg Deal Size should match the motion you claim. Mix shift changes coverage and capacity math.",
14503
+ ops: "Mean closed-won amount. Segment before coaching on size."
14504
+ },
14505
+ aliases: ["average deal size", "asp", "acv"]
14506
+ },
14507
+ {
14508
+ id: "avg_sales_cycle",
14509
+ kind: "saas",
14510
+ label: "Avg Sales Cycle",
14511
+ group: "Sales Efficiency",
14512
+ tagline: "How long from create to close on wins?",
14513
+ how_computed: "Mean days from created_at to close date on dated closed-won deals.",
14514
+ formula_lines: ["Avg Cycle = mean(close_date \u2212 created_at) on dated wins"],
14515
+ meaning: 'Board question: "is the cycle stretching \u2014 the earliest soft signal of deal-quality decay?"',
14516
+ expert_read: "Cycle stretching past the motion's norm is the earliest soft signal of deal-quality decay. Pair with Flow Rate stuck stages.",
14517
+ deepdive: [
14518
+ "Feeds Pipeline Velocity as the denominator.",
14519
+ "Needs dated wins \u2014 missing close dates understate/omit."
14520
+ ],
14521
+ visual: {
14522
+ kind: "bars",
14523
+ caption: "Exemplar cycle vs motion norm",
14524
+ bars: [
14525
+ { label: "Your cycle", value: 78, tone: "yellow" },
14526
+ { label: "Motion norm", value: 55, tone: "green" }
14527
+ ]
14528
+ },
14529
+ audience: {
14530
+ board: "Cycle stretch is an early soft signal that quality or process is slipping \u2014 before the miss shows in bookings.",
14531
+ ops: "Mean create\u2192close on dated wins. Investigate the stage that aged."
14532
+ },
14533
+ aliases: ["sales cycle", "cycle length", "time to close"]
14534
+ },
14535
+ {
14536
+ id: "stage_conversion",
14537
+ kind: "saas",
14538
+ label: "Stage Conversion",
14539
+ group: "Sales Efficiency",
14540
+ tagline: "Where in the stage model does advancement collapse?",
14541
+ how_computed: "From metadata.stage_history stage advances when present; otherwise a win-rate proxy.",
14542
+ formula_lines: [
14543
+ "Preferred: advancement rates from stage_history",
14544
+ "Fallback: win-rate proxy when history is missing"
14545
+ ],
14546
+ meaning: 'Board question: "which single stage is starving everything downstream?"',
14547
+ expert_read: "Find the one stage where conversion collapses \u2014 that's the process problem; everything downstream is starvation.",
14548
+ deepdive: [
14549
+ "Best with stage_history metadata; otherwise treat as proxy.",
14550
+ "Pairs with Flow Rate stage-age cuts."
14551
+ ],
14552
+ visual: {
14553
+ kind: "funnel",
14554
+ caption: "Exemplar \u2014 find the collapse",
14555
+ funnel: [
14556
+ { label: "Stage 1\u21922", widthPct: 100 },
14557
+ { label: "Stage 2\u21923", widthPct: 72 },
14558
+ { label: "Stage 3\u21924", widthPct: 28 },
14559
+ { label: "Stage 4\u2192Close", widthPct: 18 }
14560
+ ]
14561
+ },
14562
+ audience: {
14563
+ board: "Stage Conversion names the bottleneck stage \u2014 one collapse starves every stage after it.",
14564
+ ops: "Prefer stage_history advances. Fix the collapse stage before coaching downstream reps."
14565
+ },
14566
+ aliases: ["stage conversion", "stage advance", "conversion by stage"]
14567
+ },
14568
+ // —— Unit economics ——
14569
+ {
14570
+ id: "ltv_proxy",
14571
+ kind: "saas",
14572
+ label: "LTV (Proxy)",
14573
+ group: "Unit Economics",
14574
+ tagline: "Rough lifetime value from deal size and GRR.",
14575
+ how_computed: "avgDeal / ((100 \u2212 GRR) / 100) when GRR < 100. Unavailable when GRR is 100%+ or missing.",
14576
+ formula_lines: [
14577
+ "LTV \u2248 avgDeal / churnRate",
14578
+ "churnRate = (100 \u2212 GRR) / 100 (requires GRR < 100)"
14579
+ ],
14580
+ meaning: 'Board question: "what is a customer roughly worth over their life?"',
14581
+ expert_read: "This is a proxy \u2014 not a cohort LTV. Use it for direction, not capital allocation.",
14582
+ deepdive: [
14583
+ "Unavailable when GRR \u2265 100 or missing.",
14584
+ "Pairs with CAC for LTV:CAC when spend data exists."
14585
+ ],
14586
+ visual: {
14587
+ kind: "gauge",
14588
+ caption: "Exemplar LTV proxy (directional)",
14589
+ gauge: 68
14590
+ },
14591
+ audience: {
14592
+ board: "LTV Proxy is directional from deal size and GRR \u2014 not a cohort LTV. Use for orientation, not capital decisions.",
14593
+ ops: "avgDeal / ((100\u2212GRR)/100). Needs GRR < 100. Prefer cohort math when billing data arrives."
14594
+ },
14595
+ aliases: ["ltv", "lifetime value"]
14596
+ },
14597
+ {
14598
+ id: "cac",
14599
+ kind: "saas",
14600
+ label: "CAC",
14601
+ group: "Unit Economics",
14602
+ tagline: "Customer acquisition cost \u2014 needs spend data.",
14603
+ how_computed: "Requires campaign / sales spend data. Currently unavailable on CRM-only datasets.",
14604
+ formula_lines: ["CAC = sales & marketing spend / new customers", "(requires spend data \u2014 not in CRM-only exports)"],
14605
+ meaning: 'Board question: "what does a new logo cost to win?"',
14606
+ expert_read: "Without spend, NTRP cannot invent CAC. Wire campaign spend or finance exports to unlock unit economics.",
14607
+ deepdive: [
14608
+ "Always unavailable on CRM-only demos \u2014 expected.",
14609
+ "Unlocks LTV:CAC, Payback, Magic Number when spend lands."
14610
+ ],
14611
+ visual: { kind: "none", caption: "Needs campaign spend / finance export" },
14612
+ audience: {
14613
+ board: "CAC is locked until spend data is connected \u2014 CRM alone cannot price acquisition.",
14614
+ ops: "Bring campaign or S&M spend. Until then unit-econ metrics stay unavailable by design."
14615
+ },
14616
+ aliases: ["customer acquisition cost", "acquisition cost"]
14617
+ },
14618
+ {
14619
+ id: "ltv_cac_ratio",
14620
+ kind: "saas",
14621
+ label: "LTV:CAC Ratio",
14622
+ group: "Unit Economics",
14623
+ tagline: "Is acquisition spend earning its keep?",
14624
+ how_computed: "LTV proxy \xF7 CAC. Unavailable without spend (CAC).",
14625
+ formula_lines: ["LTV:CAC = LTV_proxy / CAC", "(requires CAC)"],
14626
+ meaning: 'Board question: "do we earn enough lifetime value per dollar spent to acquire?"',
14627
+ expert_read: "Efficiency era: boards weigh LTV:CAC and payback as heavily as growth. Classic rule of thumb \u22653x, but motion and gross margin matter.",
14628
+ deepdive: ["Blocked on CAC. See LTV Proxy and CAC."],
14629
+ visual: { kind: "none", caption: "Needs CAC (spend data)" },
14630
+ audience: {
14631
+ board: "LTV:CAC is the acquisition ROI story \u2014 available once spend is wired.",
14632
+ ops: "LTV_proxy / CAC. Unlocks with spend import."
14633
+ },
14634
+ aliases: ["ltv cac", "ltv/cac", "ltv to cac"]
14635
+ },
14636
+ {
14637
+ id: "payback_months",
14638
+ kind: "saas",
14639
+ label: "Payback Months",
14640
+ group: "Unit Economics",
14641
+ tagline: "How many months to recover CAC?",
14642
+ how_computed: "Requires CAC / spend. Lower is better.",
14643
+ formula_lines: ["Payback \u2248 CAC / (monthly gross profit per customer)", "(requires spend data)"],
14644
+ meaning: 'Board question: "how fast does acquisition spend return?" Efficiency era prior: <18 months often healthy.',
14645
+ expert_read: "Boards now weigh payback (<18mo) as heavily as growth in many motions.",
14646
+ deepdive: ["Blocked on CAC. Benchmarks exist per motion once data lands."],
14647
+ visual: { kind: "none", caption: "Needs CAC (spend data)" },
14648
+ audience: {
14649
+ board: "Payback is how fast CAC returns. Efficiency-era boards often want <18 months.",
14650
+ ops: "Requires CAC. Motion green/yellow bands apply when available."
14651
+ },
14652
+ aliases: ["payback", "cac payback"],
14653
+ benchmarkHint: (motion) => monthsBand(motion)
14654
+ },
14655
+ {
14656
+ id: "magic_number",
14657
+ kind: "saas",
14658
+ label: "Magic Number",
14659
+ group: "Unit Economics",
14660
+ tagline: "Sales efficiency \u2014 net new ARR per sales dollar.",
14661
+ how_computed: "Requires sales spend. Classic form: net new ARR (quarter) / prior-quarter S&M spend.",
14662
+ formula_lines: [
14663
+ "Magic Number \u2248 Net New ARR(q) / S&M spend(q\u22121)",
14664
+ "(requires spend data)"
14665
+ ],
14666
+ meaning: 'Board question: "how efficiently does sales spend produce net new ARR?" Prior: >0.75 often healthy; >1 strong.',
14667
+ expert_read: "Efficiency era: magic number >0.75 is weighed alongside growth. Without spend, NTRP keeps this unavailable rather than inventing it.",
14668
+ deepdive: ["Blocked on spend. Benchmarks per motion ready when data lands."],
14669
+ visual: { kind: "none", caption: "Needs S&M spend data" },
14670
+ audience: {
14671
+ board: "Magic Number prices sales efficiency. Available once S&M spend is connected.",
14672
+ ops: "Net new ARR / prior S&M. Motion benchmarks apply when spend lands."
14673
+ },
14674
+ aliases: ["sales magic number", "sales efficiency magic number"],
14675
+ benchmarkHint: (motion) => magicBand(motion)
14676
+ }
14677
+ ];
14678
+ METRIC_DEFINITIONS = [...VITALS, ...SAAS];
14679
+ BY_ID = new Map(METRIC_DEFINITIONS.map((m) => [m.id, m]));
14680
+ ALIAS_INDEX = (() => {
14681
+ const idx = /* @__PURE__ */ new Map();
14682
+ for (const m of METRIC_DEFINITIONS) {
14683
+ idx.set(m.id.toLowerCase(), m.id);
14684
+ idx.set(m.label.toLowerCase(), m.id);
14685
+ for (const a of m.aliases ?? []) {
14686
+ idx.set(a.toLowerCase(), m.id);
14687
+ }
14688
+ }
14689
+ idx.set("signal-to-noise", "signal_to_noise");
14690
+ idx.set("signal:noise", "signal_to_noise");
14691
+ idx.set("flow-rate", "flow_rate");
14692
+ idx.set("drop-rate", "drop_rate");
14693
+ idx.set("thread-depth", "thread_depth");
14694
+ return idx;
14695
+ })();
14696
+ SAAS_METRIC_IDS = SAAS.map((m) => m.id);
14697
+ }
14698
+ });
14699
+
14700
+ // src/services/metric-explainers.ts
14701
+ function normalizeAudience(audience) {
14702
+ if (!audience) return "board";
14703
+ const a = String(audience).toLowerCase();
14704
+ if (a === "ops" || a === "operations" || a === "operator" || a === "team") {
14705
+ return "ops";
13534
14706
  }
13535
- return `No analysis found. Run ${paint("accent", "/new")} or ${paint("accent", "/diagnose")} first.`;
14707
+ return "board";
13536
14708
  }
13537
- function formatMetricLine(row) {
13538
- const label = row.label ?? row.metric;
13539
- const formatted = row.formatted ?? "--";
13540
- const conf = row.confidence;
13541
- const confStr = conf != null && conf < 80 ? ` (${conf}% conf)` : "";
13542
- return `- ${label}: ${formatted}${confStr}`;
14709
+ function audienceLabel(audience) {
14710
+ return audience === "ops" ? "ops" : "board / exec";
13543
14711
  }
13544
- function buildHandoffContextBlock(bundle, ctx) {
13545
- const { diagnosis, metrics } = bundle;
13546
- const profile = loadProfile();
13547
- const lines = [];
13548
- if (profile?.company_name) {
13549
- lines.push(`Company: ${profile.company_name} (${profile.industry})`);
13550
- lines.push(`Sales motion: ${profile.sales_motion}${profile.average_deal_size ? ` \xB7 avg deal ${profile.average_deal_size}` : ""}`);
13551
- if (profile.user_scope) lines.push(`My scope: ${profile.user_scope}`);
13552
- }
13553
- if (ctx.dataset?.label) {
13554
- const counts = ctx.dataset.counts ?? {};
13555
- const countStr = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k}`).join(", ");
13556
- lines.push(`Dataset: ${ctx.dataset.label}${countStr ? ` (${countStr})` : ""}`);
13557
- }
13558
- const completed = ctx.analysis.completed;
13559
- if (completed.length > 0) {
13560
- lines.push(`Analysis lenses completed: ${completed.join(", ")}`);
13561
- }
13562
- lines.push("");
13563
- if (diagnosis) {
13564
- const { health, findings } = diagnosis;
13565
- lines.push("## GTM health (vital signs)");
13566
- lines.push(`Overall score: ${Math.round(health.overall_score)} (${health.overall_status})`);
13567
- if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
13568
- lines.push(`Total value at risk: ${formatCurrency(health.total_value_at_risk)}`);
13569
- }
13570
- lines.push("");
13571
- lines.push("### Vital signs");
13572
- for (const vs of health.vital_signs) {
13573
- const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
13574
- const dollars = vs.dollar_value != null ? ` \u2014 ${formatCurrency(vs.dollar_value)}${vs.dollar_label ? ` ${vs.dollar_label}` : ""}` : "";
13575
- lines.push(`- ${label}: ${Math.round(vs.score)} (${vs.status})${dollars}`);
14712
+ function statusRankFrom(status) {
14713
+ if (status === "red") return 0;
14714
+ if (status === "yellow") return 1;
14715
+ if (status === "green") return 2;
14716
+ return 3;
14717
+ }
14718
+ function collectCandidates(bundle, opts) {
14719
+ const prefer = new Set(opts.prefer ?? []);
14720
+ const map = /* @__PURE__ */ new Map();
14721
+ const upsert = (id, priority, status) => {
14722
+ if (!getMetricExplainer(id)) return;
14723
+ const existing = map.get(id);
14724
+ const rank = statusRankFrom(status);
14725
+ if (!existing) {
14726
+ map.set(id, { id, priority, statusRank: rank });
14727
+ return;
13576
14728
  }
13577
- if (findings.length > 0) {
13578
- lines.push("");
13579
- lines.push("### GTM findings");
13580
- appendFindings(lines, findings);
14729
+ existing.priority = Math.min(existing.priority, priority);
14730
+ existing.statusRank = Math.min(existing.statusRank, rank);
14731
+ };
14732
+ const health = bundle?.diagnosis?.health;
14733
+ const fromDiag = opts.vitals ?? health?.vital_signs ?? [];
14734
+ const gating = opts.prefer?.[0] ?? health?.gating_vital_sign;
14735
+ if (gating) upsert(String(gating), 0, "red");
14736
+ for (const vs of fromDiag) {
14737
+ const id = vs.vital_sign;
14738
+ upsert(id, prefer.has(id) ? 0 : 1, vs.status);
14739
+ }
14740
+ const rawMetrics = opts.metrics ?? bundle?.metrics?.metrics ?? [];
14741
+ let sawMetrics = rawMetrics.length > 0;
14742
+ for (const row of rawMetrics) {
14743
+ const id = String(row.metric ?? row.metric ?? "");
14744
+ if (!id) continue;
14745
+ const status = String(row.status ?? row.status ?? "");
14746
+ const value = row.value ?? row.value;
14747
+ const unavailable = row.unavailable_reason ?? row.unavailable_reason;
14748
+ if (value == null && unavailable) continue;
14749
+ upsert(id, prefer.has(id) ? 0 : 2, status);
14750
+ }
14751
+ if (sawMetrics || bundle?.metrics) {
14752
+ for (const id of ["arr", "nrr", "pipeline_coverage", "win_rate"]) {
14753
+ if (!map.has(id) && getMetricExplainer(id)) {
14754
+ upsert(id, 3, "neutral");
14755
+ }
13581
14756
  }
13582
- lines.push("");
13583
14757
  }
13584
- if (metrics && metrics.metrics.length > 0) {
13585
- lines.push("## SaaS metrics");
13586
- const byKey = new Map(metrics.metrics.map((r) => [r.metric, r]));
13587
- for (const key of KEY_METRICS) {
13588
- const row = byKey.get(key);
13589
- if (row) lines.push(formatMetricLine(row));
13590
- }
13591
- if (metrics.findings.length > 0) {
13592
- lines.push("");
13593
- lines.push("### Metrics findings");
13594
- appendFindings(lines, metrics.findings);
14758
+ if (map.size === 0) {
14759
+ for (const id of ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"]) {
14760
+ upsert(id, 4, "neutral");
13595
14761
  }
13596
- lines.push("");
13597
- }
13598
- if (!diagnosis && metrics) {
13599
- lines.unshift("Primary analysis: SaaS metrics (no GTM health snapshot on this session yet).", "");
13600
- } else if (diagnosis && !metrics) {
13601
- lines.push("(SaaS metrics not run on this session \u2014 run /metrics for the revenue view)");
13602
14762
  }
13603
- return lines.join("\n").trim();
14763
+ return [...map.values()].sort((a, b) => {
14764
+ if (a.priority !== b.priority) return a.priority - b.priority;
14765
+ if (a.statusRank !== b.statusRank) return a.statusRank - b.statusRank;
14766
+ return a.id.localeCompare(b.id);
14767
+ });
13604
14768
  }
13605
- function buildExploreContextBlock(bundle, ctx) {
13606
- const full = buildHandoffContextBlock(bundle, ctx);
13607
- if (!full) return "";
13608
- const lines = full.split("\n");
13609
- const out = [
13610
- "COMPLETED ANALYSIS (the user already saw the full report \u2014 cite this, do not re-dump it):",
13611
- ""
13612
- ];
13613
- let inFindings = false;
13614
- let findingCount = 0;
13615
- for (const line of lines) {
13616
- if (line.startsWith("### GTM findings") || line.startsWith("### Metrics findings")) {
13617
- inFindings = true;
13618
- out.push(line);
13619
- continue;
13620
- }
13621
- if (inFindings && line.startsWith("- [")) {
13622
- if (findingCount >= 5) continue;
13623
- out.push(line);
13624
- findingCount++;
13625
- continue;
13626
- }
13627
- if (inFindings && line.startsWith("##")) {
13628
- inFindings = false;
13629
- }
13630
- if (line.startsWith("## ") || line.startsWith("### Vital") || line.startsWith("- ") && !inFindings) {
13631
- if (line.startsWith("(SaaS metrics not run")) continue;
13632
- out.push(line);
13633
- }
13634
- if (line.startsWith("Overall score:") || line.startsWith("Total value at risk:")) {
13635
- out.push(line);
13636
- }
13637
- if (line.startsWith("- ARR:") || line.startsWith("- NRR:") || line.startsWith("- GRR:")) {
13638
- out.push(line);
14769
+ function formatEntry(explainer, audience) {
14770
+ const framing = audience === "ops" ? explainer.audience.ops : explainer.audience.board;
14771
+ const lines = [];
14772
+ lines.push(`### ${explainer.label} (\`${explainer.id}\`)`);
14773
+ lines.push("");
14774
+ lines.push(framing);
14775
+ lines.push("");
14776
+ if (audience === "ops") {
14777
+ lines.push("**How it's calculated**");
14778
+ lines.push("");
14779
+ for (const f of explainer.formula_lines) {
14780
+ lines.push(`- \`${f}\``);
13639
14781
  }
14782
+ lines.push("");
14783
+ } else {
14784
+ lines.push(`*${explainer.tagline}*`);
14785
+ lines.push("");
13640
14786
  }
13641
- return out.join("\n").trim();
13642
- }
13643
- function appendFindings(lines, findings) {
13644
- for (const f of findings.slice(0, 12)) {
13645
- const dollars = f.dollar_value != null ? ` (${formatCurrency(f.dollar_value)})` : "";
13646
- const plays = f.recommended_plays?.length ? ` \u2192 Plays: ${f.recommended_plays.map((p) => p.play_name).join(", ")}` : "";
13647
- lines.push(`- [${f.severity}]${dollars} ${f.finding}${plays}`);
13648
- }
14787
+ return lines;
13649
14788
  }
13650
- function handoffInstructionPrefix(primary) {
13651
- if (primary === "revenue_metrics") {
13652
- return "the SaaS metrics and pipeline context below";
14789
+ function buildDefinitionsAppendix(bundle, opts = {}) {
14790
+ const audience = normalizeAudience(opts.audience);
14791
+ const cap = opts.cap ?? DEFAULT_CAP;
14792
+ const candidates = collectCandidates(bundle, opts).slice(0, cap);
14793
+ if (candidates.length === 0) return "";
14794
+ const lines = [];
14795
+ lines.push(`## Metric definitions (for the ${audienceLabel(audience)})`);
14796
+ lines.push("");
14797
+ lines.push(
14798
+ audience === "ops" ? "Formula-first brief for operators executing the plan. Full slides: `/deepdive <metric>`." : "Meaning-first brief for the room. Full slides: `/deepdive <metric>`."
14799
+ );
14800
+ lines.push("");
14801
+ for (const c of candidates) {
14802
+ const explainer = getMetricExplainer(c.id);
14803
+ if (!explainer) continue;
14804
+ lines.push(...formatEntry(explainer, audience));
13653
14805
  }
13654
- return "the pipeline diagnosis below";
14806
+ return lines.join("\n");
13655
14807
  }
13656
- var KEY_METRICS;
13657
- var init_session_analysis = __esm({
13658
- "src/services/session-analysis.ts"() {
14808
+ var DEFAULT_CAP;
14809
+ var init_metric_explainers = __esm({
14810
+ "src/services/metric-explainers.ts"() {
13659
14811
  "use strict";
13660
- init_queries();
13661
- init_profile();
13662
- init_formatters();
13663
- init_theme();
13664
- KEY_METRICS = ["arr", "nrr", "grr", "win_rate", "pipeline_coverage"];
14812
+ init_metric_definitions();
14813
+ DEFAULT_CAP = 8;
13665
14814
  }
13666
14815
  });
13667
14816
 
@@ -13703,16 +14852,24 @@ function buildOpenQuestions(ctx) {
13703
14852
  }
13704
14853
  return lines.length > 0 ? lines.join("\n") : "(No open questions recorded.)";
13705
14854
  }
13706
- function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions, ctx) {
14855
+ function audiencePhrase(audience) {
14856
+ if (!audience) return "an executive audience";
14857
+ const a = audience.toLowerCase();
14858
+ if (a === "ops" || a === "operations") return "an ops / operator audience";
14859
+ if (a === "board") return "a board / exec audience";
14860
+ return `a ${audience} audience`;
14861
+ }
14862
+ function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions, definitionsBlock, ctx) {
13707
14863
  const company = loadProfile()?.company_name ?? "the company";
13708
14864
  const contextLabel = handoffInstructionPrefix(ctx.analysis.primary);
14865
+ const forWhom = audiencePhrase(ctx.scope?.audience);
13709
14866
  const instructions = {
13710
- deck: `produce an executive review deck outline for ${company}. Structure: (1) Headline number; (2) Pipeline health; (3) Top 3 risks with dollar impact; (4) Recommended plays; (5) Asks / decisions.`,
13711
- asana: `produce an Asana project plan with sections and tasks tied to findings. Prioritize by dollar impact.`,
13712
- clay: `produce a Clay table specification to operationalize the highest-impact finding.`,
13713
- plan: `produce a prioritized action plan with problem, play, first 3 steps, owner, and leading indicator per item.`
14867
+ deck: `produce an executive review deck outline for ${company}, framed for ${forWhom}. Structure: (1) Headline number; (2) Pipeline health; (3) Top 3 risks with dollar impact; (4) Recommended plays; (5) Asks / decisions. Use the Metric definitions appendix so every slide can briefly remind the room what the number means for this audience.`,
14868
+ asana: `produce an Asana project plan with sections and tasks tied to findings for ${forWhom}. Prioritize by dollar impact. Reference metric definitions when a task owner needs to know what "good" looks like.`,
14869
+ clay: `produce a Clay table specification to operationalize the highest-impact finding for ${forWhom}.`,
14870
+ plan: `produce a prioritized action plan for ${forWhom} with problem, play, first 3 steps, owner, and leading indicator per item. Ground indicators in the Metric definitions appendix.`
13714
14871
  };
13715
- return [
14872
+ const parts = [
13716
14873
  `# NTRP handoff \u2192 ${target}`,
13717
14874
  "",
13718
14875
  `You are an expert GTM operator. Using ${contextLabel}, ${instructions[target]}`,
@@ -13728,25 +14885,35 @@ function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions,
13728
14885
  conversationBlock,
13729
14886
  "",
13730
14887
  "---",
13731
- "",
13732
- "## Open questions",
13733
- "",
13734
- openQuestions,
13735
- "",
13736
- "---",
13737
14888
  ""
13738
- ].join("\n");
14889
+ ];
14890
+ if (definitionsBlock.trim()) {
14891
+ parts.push(definitionsBlock.trim(), "", "---", "");
14892
+ }
14893
+ parts.push("## Open questions", "", openQuestions, "", "---", "");
14894
+ return parts.join("\n");
13739
14895
  }
13740
14896
  async function buildDeliverableDraft(ctx, target = "plan") {
13741
14897
  const bundle = await loadSessionAnalysisBundle();
13742
14898
  const analysis = buildHandoffContextBlock(bundle, ctx);
13743
14899
  const conversation = buildConversationSection(ctx);
13744
14900
  const open_questions = buildOpenQuestions(ctx);
14901
+ const definitions = buildDefinitionsAppendix(bundle, {
14902
+ audience: ctx.scope?.audience,
14903
+ prefer: bundle.diagnosis?.health.gating_vital_sign ? [bundle.diagnosis.health.gating_vital_sign] : void 0
14904
+ });
13745
14905
  if (!analysis && ctx.messages.length === 0) return null;
13746
- const markdown = wrapForTarget(target, analysis, conversation, open_questions, ctx);
14906
+ const markdown = wrapForTarget(
14907
+ target,
14908
+ analysis,
14909
+ conversation,
14910
+ open_questions,
14911
+ definitions,
14912
+ ctx
14913
+ );
13747
14914
  return {
13748
14915
  markdown,
13749
- sections: { analysis, conversation, open_questions }
14916
+ sections: { analysis, conversation, open_questions, definitions }
13750
14917
  };
13751
14918
  }
13752
14919
  function inferHandoffTarget(input) {
@@ -13766,6 +14933,7 @@ var init_handoff_draft = __esm({
13766
14933
  "use strict";
13767
14934
  init_profile();
13768
14935
  init_session_analysis();
14936
+ init_metric_explainers();
13769
14937
  QUESTION_LEAD_RE = /^\s*(what|why|how|when|where|who|which|is|are|was|were|do|does|did|explain|tell me|help me understand)\b/i;
13770
14938
  SHIP_INTENT_RE = /\b(ship|export|deliver|write[- ]?up|board memo|action plan|turn (this|it|that) into|(draft|create|make|build|prepare|generate|send)\s+(me\s+)?(a\s+|the\s+)?hand[- ]?off|hand[- ]?off\s+(prompt|doc|document|plan))\b/i;
13771
14939
  }
@@ -14991,94 +16159,94 @@ var init_strategist2 = __esm({
14991
16159
  });
14992
16160
 
14993
16161
  // src/output/strategy-brief.ts
14994
- import chalk12 from "chalk";
16162
+ import chalk13 from "chalk";
14995
16163
  function printWrapped(text, width, prefix = INDENT, style) {
14996
16164
  for (const line of wrapWords(text, width)) {
14997
16165
  console.log(prefix + (style ? style(line) : line));
14998
16166
  }
14999
16167
  }
15000
16168
  function outcomeLine(outcome) {
15001
- return `${chalk12.bold(outcome.metric)}: ${outcome.baseline} ${chalk12.dim("->")} ${chalk12.bold(outcome.target_range)} ${chalk12.dim(`by ${outcome.check_date} \xB7 ${outcome.measured_by}`)}`;
16169
+ return `${chalk13.bold(outcome.metric)}: ${outcome.baseline} ${chalk13.dim("->")} ${chalk13.bold(outcome.target_range)} ${chalk13.dim(`by ${outcome.check_date} \xB7 ${outcome.measured_by}`)}`;
15002
16170
  }
15003
16171
  function printWorkstream(ws, width) {
15004
- const plays = ws.play_ids.length > 0 ? chalk12.dim(` play: ${ws.play_ids.join(", ")}`) : "";
15005
- console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${chalk12.bold(ws.title)}${plays}`);
15006
- printWrapped(ws.problem, width - 5, INDENT + " ", (s) => chalk12.dim(s));
16172
+ const plays = ws.play_ids.length > 0 ? chalk13.dim(` play: ${ws.play_ids.join(", ")}`) : "";
16173
+ console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${chalk13.bold(ws.title)}${plays}`);
16174
+ printWrapped(ws.problem, width - 5, INDENT + " ", (s) => chalk13.dim(s));
15007
16175
  if (ws.rationale) {
15008
- printWrapped(`Why now: ${ws.rationale}`, width - 5, INDENT + " ", (s) => chalk12.dim(s));
16176
+ printWrapped(`Why now: ${ws.rationale}`, width - 5, INDENT + " ", (s) => chalk13.dim(s));
15009
16177
  }
15010
16178
  console.log(`${INDENT} ${outcomeLine(ws.expected_outcome)}`);
15011
16179
  for (const li of ws.leading_indicators) {
15012
- console.log(`${INDENT} ${chalk12.dim("leads:")} ${outcomeLine(li)}`);
16180
+ console.log(`${INDENT} ${chalk13.dim("leads:")} ${outcomeLine(li)}`);
15013
16181
  }
15014
16182
  if (ws.milestones.length > 0) {
15015
- console.log(`${INDENT} ${chalk12.dim("Milestones")}`);
16183
+ console.log(`${INDENT} ${chalk13.dim("Milestones")}`);
15016
16184
  for (const m of ws.milestones) {
15017
- console.log(`${INDENT} ${paint("accent", m.due)} ${m.label} ${chalk12.dim(`(verify: ${m.verification})`)}`);
16185
+ console.log(`${INDENT} ${paint("accent", m.due)} ${m.label} ${chalk13.dim(`(verify: ${m.verification})`)}`);
15018
16186
  }
15019
16187
  }
15020
16188
  if (ws.deliverables.length > 0) {
15021
- console.log(`${INDENT} ${chalk12.dim("Deliverables")}`);
16189
+ console.log(`${INDENT} ${chalk13.dim("Deliverables")}`);
15022
16190
  for (const d of ws.deliverables) {
15023
- console.log(`${INDENT} ${chalk12.dim("[ ]")} ${d.label} ${chalk12.dim(`(${d.kind.replace("_", " ")} \xB7 due ${d.due})`)}`);
16191
+ console.log(`${INDENT} ${chalk13.dim("[ ]")} ${d.label} ${chalk13.dim(`(${d.kind.replace("_", " ")} \xB7 due ${d.due})`)}`);
15024
16192
  }
15025
16193
  }
15026
16194
  if (ws.actions.length > 0) {
15027
- console.log(`${INDENT} ${chalk12.dim("First actions")}`);
16195
+ console.log(`${INDENT} ${chalk13.dim("First actions")}`);
15028
16196
  for (const action of ws.actions.slice(0, 4)) {
15029
- printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) => chalk12.dim(s));
16197
+ printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) => chalk13.dim(s));
15030
16198
  }
15031
16199
  }
15032
16200
  printWrapped(
15033
16201
  `If ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) -> ${ws.contingency.fallback}`,
15034
16202
  width - 5,
15035
16203
  INDENT + " ",
15036
- (s) => chalk12.hex("#eab308")(s)
16204
+ (s) => chalk13.hex("#eab308")(s)
15037
16205
  );
15038
- console.log(`${INDENT} ${chalk12.dim(`~${Math.round(ws.effort_hours)} team-hours`)}`);
16206
+ console.log(`${INDENT} ${chalk13.dim(`~${Math.round(ws.effort_hours)} team-hours`)}`);
15039
16207
  console.log();
15040
16208
  }
15041
16209
  function printStrategyBrief(plan, stats) {
15042
16210
  const width = Math.min(termWidth() - 4, 92);
15043
16211
  console.log();
15044
16212
  console.log(
15045
- `${INDENT}${chalk12.bold(`Strategy brief \u2014 ${plan.title}`)} ${chalk12.dim(`confidence ${plan.confidence.toFixed(2)} \xB7 ${plan.priority} priority \xB7 review ${plan.review_cadence.toLowerCase()}`)}`
16213
+ `${INDENT}${chalk13.bold(`Strategy brief \u2014 ${plan.title}`)} ${chalk13.dim(`confidence ${plan.confidence.toFixed(2)} \xB7 ${plan.priority} priority \xB7 review ${plan.review_cadence.toLowerCase()}`)}`
15046
16214
  );
15047
- console.log(INDENT + chalk12.dim(hr(width)));
16215
+ console.log(INDENT + chalk13.dim(hr(width)));
15048
16216
  printWrapped(`Objective: ${plan.objective}`, width, INDENT, (s) => paint("accent", s));
15049
16217
  console.log();
15050
- console.log(`${INDENT}${chalk12.dim("30,000 ft")}`);
16218
+ console.log(`${INDENT}${chalk13.dim("30,000 ft")}`);
15051
16219
  printWrapped(plan.summary_30k, width);
15052
16220
  console.log();
15053
16221
  for (const ws of plan.workstreams) {
15054
16222
  printWorkstream(ws, width);
15055
16223
  }
15056
16224
  if (plan.constraints.length > 0) {
15057
- console.log(`${INDENT}${chalk12.dim("Constraints")}`);
16225
+ console.log(`${INDENT}${chalk13.dim("Constraints")}`);
15058
16226
  for (const c of plan.constraints) {
15059
- printWrapped(`- ${c}`, width - 2, INDENT, (s) => chalk12.dim(s));
16227
+ printWrapped(`- ${c}`, width - 2, INDENT, (s) => chalk13.dim(s));
15060
16228
  }
15061
16229
  console.log();
15062
16230
  }
15063
16231
  if (plan.assumptions.length > 0) {
15064
- console.log(`${INDENT}${chalk12.dim("Assumptions (unverified \u2014 not counted as targets)")}`);
16232
+ console.log(`${INDENT}${chalk13.dim("Assumptions (unverified \u2014 not counted as targets)")}`);
15065
16233
  for (const a of plan.assumptions) {
15066
- printWrapped(`- ${a}`, width - 2, INDENT, (s) => chalk12.dim(s));
16234
+ printWrapped(`- ${a}`, width - 2, INDENT, (s) => chalk13.dim(s));
15067
16235
  }
15068
16236
  console.log();
15069
16237
  }
15070
16238
  if (plan.risks.length > 0) {
15071
- console.log(`${INDENT}${chalk12.dim("Risks")}`);
16239
+ console.log(`${INDENT}${chalk13.dim("Risks")}`);
15072
16240
  for (const r of plan.risks) {
15073
- printWrapped(`- ${r}`, width - 2, INDENT, (s) => chalk12.dim(s));
16241
+ printWrapped(`- ${r}`, width - 2, INDENT, (s) => chalk13.dim(s));
15074
16242
  }
15075
16243
  console.log();
15076
16244
  }
15077
16245
  const totalHours = plan.workstreams.reduce((sum, ws) => sum + ws.effort_hours, 0);
15078
- console.log(INDENT + chalk12.dim(hr(width)));
16246
+ console.log(INDENT + chalk13.dim(hr(width)));
15079
16247
  const coverage = stats.total_targets > 0 ? `${stats.measurable_targets} of ${stats.total_targets} targets measurable with current data` : "no quantified targets";
15080
- const coverageStyled = stats.total_targets > 0 && stats.measurable_targets === stats.total_targets ? paint("success", coverage) : chalk12.hex("#eab308")(coverage);
15081
- console.log(`${INDENT}${coverageStyled}${chalk12.dim(` \xB7 ~${Math.round(totalHours)} total team-hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
16248
+ const coverageStyled = stats.total_targets > 0 && stats.measurable_targets === stats.total_targets ? paint("success", coverage) : chalk13.hex("#eab308")(coverage);
16249
+ console.log(`${INDENT}${coverageStyled}${chalk13.dim(` \xB7 ~${Math.round(totalHours)} total team-hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
15082
16250
  console.log();
15083
16251
  }
15084
16252
  var INDENT;
@@ -15103,7 +16271,7 @@ __export(strategist_flow_exports, {
15103
16271
  resumeStrategistAfterConnect: () => resumeStrategistAfterConnect,
15104
16272
  startStrategistFlow: () => startStrategistFlow
15105
16273
  });
15106
- import chalk13 from "chalk";
16274
+ import chalk14 from "chalk";
15107
16275
  function isStrategistIntent(input) {
15108
16276
  const line = input.trim();
15109
16277
  if (!line) return false;
@@ -15123,11 +16291,11 @@ function queueStrategistForAnalysis(ctx, opts) {
15123
16291
  saveSessionState(ctx);
15124
16292
  console.log();
15125
16293
  console.log(
15126
- " " + chalk13.dim("Strategy session queued \u2014 I'll build the plan once your data is analyzed.")
16294
+ " " + chalk14.dim("Strategy session queued \u2014 I'll build the plan once your data is analyzed.")
15127
16295
  );
15128
16296
  if (opts.origin !== "nl") {
15129
16297
  console.log(
15130
- " " + chalk13.dim("Tell me what to look at, paste a CSV path, or say ") + chalk13.cyan("use demo data") + chalk13.dim(".")
16298
+ " " + chalk14.dim("Tell me what to look at, paste a CSV path, or say ") + chalk14.cyan("use demo data") + chalk14.dim(".")
15131
16299
  );
15132
16300
  console.log();
15133
16301
  }
@@ -15146,7 +16314,7 @@ async function startStrategistFlow(ctx, opts) {
15146
16314
  ctx.strategistState = { step: "objective_input", origin: opts.origin };
15147
16315
  saveSessionState(ctx);
15148
16316
  console.log();
15149
- console.log(" " + chalk13.dim(`What's the objective? State it like a finish line \u2014 e.g. "cut stale pipeline in half before Q4".`));
16317
+ console.log(" " + chalk14.dim(`What's the objective? State it like a finish line \u2014 e.g. "cut stale pipeline in half before Q4".`));
15150
16318
  console.log();
15151
16319
  recordMessage(ctx, "agent", "Strategist: asked for objective");
15152
16320
  return "Awaiting objective";
@@ -15183,14 +16351,14 @@ async function handleStrategizeFlow(input, ctx) {
15183
16351
  ctx.strategistState = void 0;
15184
16352
  saveSessionState(ctx);
15185
16353
  console.log();
15186
- console.log(" " + chalk13.dim("Strategy session cancelled \u2014 back to exploring."));
16354
+ console.log(" " + chalk14.dim("Strategy session cancelled \u2014 back to exploring."));
15187
16355
  console.log();
15188
16356
  return "Strategy cancelled";
15189
16357
  }
15190
16358
  if (state2.step === "objective_input") {
15191
16359
  if (line.length < 8) {
15192
16360
  console.log();
15193
- console.log(" " + chalk13.dim("Give me a bit more \u2014 what outcome are we planning toward?"));
16361
+ console.log(" " + chalk14.dim("Give me a bit more \u2014 what outcome are we planning toward?"));
15194
16362
  console.log();
15195
16363
  return "Awaiting objective";
15196
16364
  }
@@ -15207,17 +16375,17 @@ async function handleStrategizeFlow(input, ctx) {
15207
16375
  state2.step = "objective_input";
15208
16376
  saveSessionState(ctx);
15209
16377
  console.log();
15210
- console.log(" " + chalk13.dim("What's the objective? State it like a finish line."));
16378
+ console.log(" " + chalk14.dim("What's the objective? State it like a finish line."));
15211
16379
  console.log();
15212
16380
  return "Awaiting objective";
15213
16381
  }
15214
16382
  if (QUESTION_RE.test(line)) {
15215
16383
  console.log();
15216
16384
  console.log(
15217
- " " + chalk13.dim("That looks like a question \u2014 I'm holding a strategy objective right now.")
16385
+ " " + chalk14.dim("That looks like a question \u2014 I'm holding a strategy objective right now.")
15218
16386
  );
15219
16387
  console.log(
15220
- " " + chalk13.dim("Say ") + chalk13.cyan("yes") + chalk13.dim(" to build the plan, ") + chalk13.cyan("adjust") + chalk13.dim(" to restate it, or ") + chalk13.cyan("cancel") + chalk13.dim(" to go answer questions first.")
16388
+ " " + chalk14.dim("Say ") + chalk14.cyan("yes") + chalk14.dim(" to build the plan, ") + chalk14.cyan("adjust") + chalk14.dim(" to restate it, or ") + chalk14.cyan("cancel") + chalk14.dim(" to go answer questions first.")
15221
16389
  );
15222
16390
  console.log();
15223
16391
  return "Awaiting confirm";
@@ -15230,7 +16398,7 @@ async function handleStrategizeFlow(input, ctx) {
15230
16398
  }
15231
16399
  console.log();
15232
16400
  console.log(
15233
- " " + chalk13.dim("Say ") + chalk13.cyan("yes") + chalk13.dim(" to plan, ") + chalk13.cyan("adjust") + chalk13.dim(" to restate the objective, or ") + chalk13.cyan("cancel") + chalk13.dim(".")
16401
+ " " + chalk14.dim("Say ") + chalk14.cyan("yes") + chalk14.dim(" to plan, ") + chalk14.cyan("adjust") + chalk14.dim(" to restate the objective, or ") + chalk14.cyan("cancel") + chalk14.dim(".")
15234
16402
  );
15235
16403
  console.log();
15236
16404
  return "Awaiting confirm";
@@ -15293,7 +16461,7 @@ async function runStrategistSession(ctx) {
15293
16461
  break;
15294
16462
  case "thinking":
15295
16463
  spinner.stop();
15296
- console.log(" " + chalk13.dim.italic(event.text));
16464
+ console.log(" " + chalk14.dim.italic(event.text));
15297
16465
  spinner.start();
15298
16466
  break;
15299
16467
  case "notice":
@@ -15315,17 +16483,17 @@ async function runStrategistSession(ctx) {
15315
16483
  spinner.stop();
15316
16484
  } catch (err) {
15317
16485
  spinner.fail("Strategy session failed");
15318
- console.error(" " + chalk13.red(String(err.message ?? err)));
16486
+ console.error(" " + chalk14.red(String(err.message ?? err)));
15319
16487
  ctx.strategistState = void 0;
15320
16488
  saveSessionState(ctx);
15321
16489
  console.log(
15322
- " " + chalk13.dim('Strategy session dropped \u2014 say "how should we fix this?" or run ') + paint("accent", "/strategy") + chalk13.dim(" to retry.")
16490
+ " " + chalk14.dim('Strategy session dropped \u2014 say "how should we fix this?" or run ') + paint("accent", "/strategy") + chalk14.dim(" to retry.")
15323
16491
  );
15324
16492
  console.log();
15325
16493
  return;
15326
16494
  }
15327
16495
  if (!plan) {
15328
- console.log(" " + chalk13.dim("(no plan produced)"));
16496
+ console.log(" " + chalk14.dim("(no plan produced)"));
15329
16497
  ctx.strategistState = void 0;
15330
16498
  saveSessionState(ctx);
15331
16499
  console.log();
@@ -15333,7 +16501,7 @@ async function runStrategistSession(ctx) {
15333
16501
  }
15334
16502
  printStrategyBrief(plan, stats);
15335
16503
  for (const notice of notices.slice(0, 6)) {
15336
- console.log(" " + chalk13.dim(notice));
16504
+ console.log(" " + chalk14.dim(notice));
15337
16505
  }
15338
16506
  printLlmAttribution(meta);
15339
16507
  console.log();
@@ -15358,18 +16526,18 @@ async function runStrategistSession(ctx) {
15358
16526
  creditStrategySession(ctx);
15359
16527
  console.log();
15360
16528
  console.log(" " + paint("accent", `Strategy saved: ${persisted.strategy.title}`));
15361
- console.log(" " + chalk13.dim(persisted.library_path));
16529
+ console.log(" " + chalk14.dim(persisted.library_path));
15362
16530
  console.log(
15363
- " " + chalk13.dim("Check progress anytime with ") + paint("accent", `/strategy review ${persisted.strategy.slug}`) + chalk13.dim(" \u2014 future answers will reference this plan.")
16531
+ " " + chalk14.dim("Check progress anytime with ") + paint("accent", `/strategy review ${persisted.strategy.slug}`) + chalk14.dim(" \u2014 future answers will reference this plan.")
15364
16532
  );
15365
16533
  console.log();
15366
16534
  recordMessage(ctx, "agent", `Strategy saved: ${persisted.strategy.title} (${persisted.strategy.slug})`);
15367
16535
  } catch (err) {
15368
- console.error(" " + chalk13.red(`Could not save strategy: ${String(err.message ?? err)}`));
16536
+ console.error(" " + chalk14.red(`Could not save strategy: ${String(err.message ?? err)}`));
15369
16537
  console.log();
15370
16538
  }
15371
16539
  } else {
15372
- console.log(" " + chalk13.dim("Kept as a working draft \u2014 not saved to the library."));
16540
+ console.log(" " + chalk14.dim("Kept as a working draft \u2014 not saved to the library."));
15373
16541
  console.log();
15374
16542
  recordMessage(ctx, "agent", `Strategy drafted (unsaved): ${plan.title}`);
15375
16543
  }
@@ -15398,16 +16566,16 @@ async function ensureSnapshot(ctx) {
15398
16566
  }
15399
16567
  function printObjectiveCard(ctx, objective, proposed) {
15400
16568
  console.log();
15401
- console.log(" " + chalk13.bold("Strategy session"));
16569
+ console.log(" " + chalk14.bold("Strategy session"));
15402
16570
  console.log(
15403
- " " + chalk13.dim(proposed ? "Proposed objective: " : "Objective: ") + paint("accent", objective)
16571
+ " " + chalk14.dim(proposed ? "Proposed objective: " : "Objective: ") + paint("accent", objective)
15404
16572
  );
15405
16573
  console.log(
15406
- " " + chalk13.dim("I'll ground it in your live data, sequence the fixes, set measurable milestones, and stress-test the plan.")
16574
+ " " + chalk14.dim("I'll ground it in your live data, sequence the fixes, set measurable milestones, and stress-test the plan.")
15407
16575
  );
15408
16576
  console.log();
15409
16577
  console.log(
15410
- " " + chalk13.dim("Confirm? ") + chalk13.cyan("\u23CE yes") + chalk13.dim(" \xB7 ") + chalk13.cyan("adjust") + chalk13.dim(" \xB7 ") + chalk13.cyan("cancel")
16578
+ " " + chalk14.dim("Confirm? ") + chalk14.cyan("\u23CE yes") + chalk14.dim(" \xB7 ") + chalk14.cyan("adjust") + chalk14.dim(" \xB7 ") + chalk14.cyan("cancel")
15411
16579
  );
15412
16580
  console.log();
15413
16581
  }
@@ -15428,35 +16596,35 @@ async function printKeylessSkeletonPlan(ctx, objective) {
15428
16596
  LAYERS2
15429
16597
  );
15430
16598
  if (triggered.length > 0) {
15431
- console.log(" " + chalk13.bold("Skeleton plan") + chalk13.dim(" \u2014 deterministic, from your computed vitals (no AI)"));
15432
- console.log(" " + chalk13.dim(`Objective: ${objective}`));
15433
- console.log(" " + chalk13.dim("Ordered by dependency: clean data gates moving pipeline gates efficient effort."));
16599
+ console.log(" " + chalk14.bold("Skeleton plan") + chalk14.dim(" \u2014 deterministic, from your computed vitals (no AI)"));
16600
+ console.log(" " + chalk14.dim(`Objective: ${objective}`));
16601
+ console.log(" " + chalk14.dim("Ordered by dependency: clean data gates moving pipeline gates efficient effort."));
15434
16602
  console.log();
15435
16603
  triggered.forEach(({ play, vital }, index) => {
15436
16604
  const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? ` \xB7 ${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? ""}`.trimEnd() : "";
15437
16605
  console.log(
15438
- ` ${paint("accent", `${index + 1}.`)} ${chalk13.bold(play.name)} ${chalk13.dim(`(${play.id})`)}`
16606
+ ` ${paint("accent", `${index + 1}.`)} ${chalk14.bold(play.name)} ${chalk14.dim(`(${play.id})`)}`
15439
16607
  );
15440
16608
  console.log(
15441
- " " + chalk13.dim(`${vital.vital_sign} ${Math.round(vital.score)} (${vital.status})${dollar}`)
16609
+ " " + chalk14.dim(`${vital.vital_sign} ${Math.round(vital.score)} (${vital.status})${dollar}`)
15442
16610
  );
15443
- console.log(" " + chalk13.dim(`Why: ${play.why.split(". ")[0]}.`));
16611
+ console.log(" " + chalk14.dim(`Why: ${play.why.split(". ")[0]}.`));
15444
16612
  if (play.steps[0]) {
15445
- console.log(" " + chalk13.dim(`First step: ${play.steps[0]}`));
16613
+ console.log(" " + chalk14.dim(`First step: ${play.steps[0]}`));
15446
16614
  }
15447
- console.log(" " + chalk13.dim(`Expected: ${play.expected_outcome}`));
16615
+ console.log(" " + chalk14.dim(`Expected: ${play.expected_outcome}`));
15448
16616
  console.log();
15449
16617
  });
15450
16618
  } else {
15451
- console.log(" " + chalk13.bold("No plays triggered") + chalk13.dim(" \u2014 every vital sign is above its play threshold."));
16619
+ console.log(" " + chalk14.bold("No plays triggered") + chalk14.dim(" \u2014 every vital sign is above its play threshold."));
15452
16620
  console.log();
15453
16621
  }
15454
16622
  }
15455
16623
  console.log(
15456
- " " + chalk13.dim("For the full strategist \u2014 milestones, outcome ranges, contingencies \u2014 press ") + paint("accent", "\u23CE") + chalk13.dim(" to run ") + paint("accent", "/connect") + chalk13.dim(" and paste any provider's key.")
16624
+ " " + chalk14.dim("For the full strategist \u2014 milestones, outcome ranges, contingencies \u2014 press ") + paint("accent", "\u23CE") + chalk14.dim(" to run ") + paint("accent", "/connect") + chalk14.dim(" and paste any provider's key.")
15457
16625
  );
15458
16626
  console.log(
15459
- " " + chalk13.dim("Objective kept \u2014 after ") + paint("accent", "/connect") + chalk13.dim(" I'll bring back the confirm card so you can run the full plan.")
16627
+ " " + chalk14.dim("Objective kept \u2014 after ") + paint("accent", "/connect") + chalk14.dim(" I'll bring back the confirm card so you can run the full plan.")
15460
16628
  );
15461
16629
  console.log();
15462
16630
  }
@@ -15502,7 +16670,7 @@ __export(keyless_ask_exports, {
15502
16670
  isKeylessVitalsAsk: () => isKeylessVitalsAsk,
15503
16671
  tryKeylessAskAnswer: () => tryKeylessAskAnswer
15504
16672
  });
15505
- import chalk14 from "chalk";
16673
+ import chalk15 from "chalk";
15506
16674
  function isKeylessVitalsAsk(input) {
15507
16675
  return KEYLESS_ASK_RE.test(input.trim());
15508
16676
  }
@@ -15551,35 +16719,35 @@ async function tryKeylessAskAnswer(ctx, input, opts = {}) {
15551
16719
  const headline = dollarBit ? `${label} \u2014 ${dollarBit} \u2014 is the most expensive problem to solve right now.` : `${label} (score ${Math.round(primary.score)}, ${primary.status}) is the problem to fix first.`;
15552
16720
  const runners = [...aggregate.vital_signs].filter((v) => v.vital_sign !== primary.vital_sign && (v.dollar_value ?? 0) > 0).sort((a, b) => (b.dollar_value ?? 0) - (a.dollar_value ?? 0)).slice(0, 2);
15553
16721
  console.log();
15554
- console.log(" " + chalk14.bold(headline));
16722
+ console.log(" " + chalk15.bold(headline));
15555
16723
  if (opts.fromResume) {
15556
16724
  if (runners.length > 0) {
15557
16725
  console.log(
15558
- " " + chalk14.dim("Next after that: ") + chalk14.dim(runners.map(formatRunnerBit).join(" \xB7 "))
16726
+ " " + chalk15.dim("Next after that: ") + chalk15.dim(runners.map(formatRunnerBit).join(" \xB7 "))
15559
16727
  );
15560
16728
  }
15561
16729
  } else {
15562
16730
  console.log();
15563
16731
  if (gating && gating.vital_sign !== primary.vital_sign) {
15564
16732
  console.log(
15565
- " " + chalk14.dim("Gating vital: ") + paint("accent", VITAL_SIGN_LABELS[gating.vital_sign]) + chalk14.dim(` (score ${Math.round(gating.score)}) \u2014 it bounds what you can trust downstream.`)
16733
+ " " + chalk15.dim("Gating vital: ") + paint("accent", VITAL_SIGN_LABELS[gating.vital_sign]) + chalk15.dim(` (score ${Math.round(gating.score)}) \u2014 it bounds what you can trust downstream.`)
15566
16734
  );
15567
16735
  }
15568
16736
  if (runners.length > 0) {
15569
- console.log(" " + chalk14.dim("Also on the board:"));
16737
+ console.log(" " + chalk15.dim("Also on the board:"));
15570
16738
  for (const vs of runners) {
15571
- console.log(" " + chalk14.dim("\xB7 ") + formatVitalLine(vs));
16739
+ console.log(" " + chalk15.dim("\xB7 ") + formatVitalLine(vs));
15572
16740
  }
15573
16741
  }
15574
16742
  if (aggregate.total_value_at_risk != null && aggregate.total_value_at_risk > 0) {
15575
16743
  console.log(
15576
- " " + chalk14.dim("Total at risk: ") + chalk14.green(formatCurrency(aggregate.total_value_at_risk))
16744
+ " " + chalk15.dim("Total at risk: ") + chalk15.green(formatCurrency(aggregate.total_value_at_risk))
15577
16745
  );
15578
16746
  }
15579
16747
  }
15580
16748
  console.log();
15581
16749
  console.log(
15582
- " " + chalk14.dim("Press ") + paint("accent", "\u23CE") + chalk14.dim(" to connect a key (") + paint("accent", "/connect") + chalk14.dim(") for the why and the plan \u2014 I'll finish this question when you do.")
16750
+ " " + chalk15.dim("Press ") + paint("accent", "\u23CE") + chalk15.dim(" to connect a key (") + paint("accent", "/connect") + chalk15.dim(") for the why and the plan \u2014 I'll finish this question when you do.")
15583
16751
  );
15584
16752
  console.log();
15585
16753
  if (!opts.fromResume) {
@@ -15601,11 +16769,119 @@ var init_keyless_ask = __esm({
15601
16769
  }
15602
16770
  });
15603
16771
 
16772
+ // src/conversation/keyless-definitions.ts
16773
+ import chalk16 from "chalk";
16774
+ function isPossessiveMetricAsk(input) {
16775
+ return POSSESSIVE_RE.test(input.trim());
16776
+ }
16777
+ function isDefinitionAsk(input) {
16778
+ const line = input.trim();
16779
+ if (!line) return false;
16780
+ if (isPossessiveMetricAsk(line)) return false;
16781
+ return DEFINITION_RE.test(line) || MEAN_RE.test(line);
16782
+ }
16783
+ function extractDefinitionQuery(input) {
16784
+ const line = input.trim().replace(/[?.!]+$/, "");
16785
+ const mean = line.match(MEAN_RE);
16786
+ if (mean?.[1]) return cleanQuery(mean[1]);
16787
+ const how = line.match(HOW_CALC_RE);
16788
+ if (how?.[1]) return cleanQuery(how[1]);
16789
+ const what = line.match(WHAT_IS_RE);
16790
+ if (what?.[1]) return cleanQuery(what[1]);
16791
+ return void 0;
16792
+ }
16793
+ function cleanQuery(raw) {
16794
+ return raw.replace(/^(?:a|an|the|our|my)\s+/i, "").replace(/\b(metric|score|number|vital(?:\s+sign)?|kpi)\b/gi, "").replace(/\s+/g, " ").trim();
16795
+ }
16796
+ function matchDefinitionExplainer(input) {
16797
+ if (!isDefinitionAsk(input)) return void 0;
16798
+ const query = extractDefinitionQuery(input);
16799
+ if (!query) return void 0;
16800
+ const id = resolveMetricId(query);
16801
+ if (!id) {
16802
+ const tokens = query.split(/\s+/);
16803
+ for (let n = tokens.length; n >= 1; n--) {
16804
+ for (let i = 0; i + n <= tokens.length; i++) {
16805
+ const slice = tokens.slice(i, i + n).join(" ");
16806
+ const hit = resolveMetricId(slice);
16807
+ if (hit) return getMetricExplainer(hit);
16808
+ }
16809
+ }
16810
+ return void 0;
16811
+ }
16812
+ return getMetricExplainer(id);
16813
+ }
16814
+ function printWrapped2(text, indent = " ") {
16815
+ for (const line of wrapWords(text, 78)) {
16816
+ console.log(indent + line);
16817
+ }
16818
+ }
16819
+ function tryKeylessDefinitionAnswer(ctx, input) {
16820
+ const explainer = matchDefinitionExplainer(input);
16821
+ if (!explainer) return false;
16822
+ const motion = loadProfile()?.sales_motion ?? null;
16823
+ const bench = explainer.benchmarkHint?.(motion);
16824
+ console.log();
16825
+ console.log(
16826
+ " " + sectionHeading(explainer.label) + chalk16.dim(` \xB7 ${explainer.kind === "vital" ? "vital sign" : "SaaS metric"}`)
16827
+ );
16828
+ console.log(" " + chalk16.dim(explainer.tagline));
16829
+ console.log();
16830
+ console.log(" " + bold("What it means"));
16831
+ printWrapped2(explainer.meaning, " ");
16832
+ console.log();
16833
+ console.log(" " + bold("How NTRP calculates it"));
16834
+ printWrapped2(explainer.how_computed, " ");
16835
+ for (const f of explainer.formula_lines) {
16836
+ console.log(" " + paint("accent", f));
16837
+ }
16838
+ if (bench) {
16839
+ console.log();
16840
+ console.log(" " + chalk16.dim(`Benchmark \xB7 ${bench}`));
16841
+ }
16842
+ if (explainer.dollar_label) {
16843
+ console.log(
16844
+ " " + chalk16.dim(`Dollar translation \xB7 ${explainer.dollar_label}`)
16845
+ );
16846
+ }
16847
+ console.log();
16848
+ console.log(
16849
+ " " + chalk16.dim("More: ") + paint("accent", `/deepdive ${explainer.id}`) + chalk16.dim(" \xB7 full tour: ") + paint("accent", "/deepdive")
16850
+ );
16851
+ console.log();
16852
+ recordMessage(ctx, "user", input);
16853
+ recordMessage(
16854
+ ctx,
16855
+ "agent",
16856
+ `${explainer.label}: ${explainer.tagline} (keyless definition)`
16857
+ );
16858
+ saveSessionState(ctx);
16859
+ return true;
16860
+ }
16861
+ var POSSESSIVE_RE, DEFINITION_RE, MEAN_RE, HOW_CALC_RE, WHAT_IS_RE;
16862
+ var init_keyless_definitions = __esm({
16863
+ "src/conversation/keyless-definitions.ts"() {
16864
+ "use strict";
16865
+ init_context2();
16866
+ init_profile();
16867
+ init_metric_definitions();
16868
+ init_theme();
16869
+ init_layout();
16870
+ POSSESSIVE_RE = /\b(our|my|we|us|the company'?s|this (company|business|org|pipeline)|current|actual|latest)\b/i;
16871
+ DEFINITION_RE = /^(?:(?:please|can you|could you)\s+)?(?:what\s+(?:is|are|does)|what'?s|whats|define|explain|describe|how\s+(?:is|are|do(?:es)?)\s+(?:.+?\s+)?(?:calculated|computed|measured|defined)|how\s+do(?:es)?\s+(?:.+?\s+)?(?:work|get calculated)|tell me about|meaning of)\b/i;
16872
+ MEAN_RE = /\bwhat does\b(.+?)\bmean\b/i;
16873
+ HOW_CALC_RE = /\bhow (?:is|are|do(?:es)?)\b(.+?)\b(?:calculated|computed|measured|defined|work)\b/i;
16874
+ WHAT_IS_RE = /\b(?:what(?:'s|s)?|define|explain|describe|tell me about|meaning of)\s+(.+?)(?:\?|$)/i;
16875
+ }
16876
+ });
16877
+
15604
16878
  // src/conversation/orchestrator.ts
15605
- import chalk15 from "chalk";
15606
- import { writeFileSync as writeFileSync13 } from "fs";
15607
- import { join as join19 } from "path";
16879
+ import chalk17 from "chalk";
16880
+ import { writeFileSync as writeFileSync14 } from "fs";
15608
16881
  async function handleExploreWithoutKey(ctx, input) {
16882
+ if (isDefinitionAsk(input) && tryKeylessDefinitionAnswer(ctx, input)) {
16883
+ return;
16884
+ }
15609
16885
  if (isKeylessVitalsAsk(input)) {
15610
16886
  queuePendingAsk(ctx, input, "explore");
15611
16887
  const answered = await tryKeylessAskAnswer(ctx, input);
@@ -15626,14 +16902,14 @@ async function handleExploreWithoutKey(ctx, input) {
15626
16902
  }
15627
16903
  if (hits === 1) {
15628
16904
  console.log();
15629
- console.log(" " + chalk15.red("AI interpretation needs an LLM API key saved in config."));
16905
+ console.log(" " + chalk17.red("AI interpretation needs an LLM API key saved in config."));
15630
16906
  console.log(
15631
- " " + chalk15.dim("Run ") + paint("accent", "/connect") + chalk15.dim(" and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).")
16907
+ " " + chalk17.dim("Run ") + paint("accent", "/connect") + chalk17.dim(" and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).")
15632
16908
  );
15633
- console.log(" " + chalk15.dim("Number crunching works without a key \u2014 only Q&A in the REPL needs one."));
16909
+ console.log(" " + chalk17.dim("Number crunching works without a key \u2014 only Q&A in the REPL needs one."));
15634
16910
  if (ctx.pendingAsk) {
15635
16911
  console.log(
15636
- " " + chalk15.dim("Your question is queued \u2014 I'll answer it right after ") + paint("accent", "/connect") + chalk15.dim(".")
16912
+ " " + chalk17.dim("Your question is queued \u2014 I'll answer it right after ") + paint("accent", "/connect") + chalk17.dim(".")
15637
16913
  );
15638
16914
  }
15639
16915
  if (ctx.gapAudit) {
@@ -15648,16 +16924,17 @@ async function handleExploreWithoutKey(ctx, input) {
15648
16924
  return;
15649
16925
  }
15650
16926
  console.log();
15651
- console.log(" " + chalk15.yellow("Still no engine connected \u2014 Q&A stays offline until you run ") + paint("accent", "/connect") + chalk15.yellow("."));
15652
- console.log(" " + chalk15.dim("These work without one:"));
15653
- console.log(" " + paint("accent", "/playbook") + chalk15.dim(" recommended plays from your computed vitals"));
15654
- console.log(" " + chalk15.cyan('"how should we fix this?"') + chalk15.dim(" deterministic skeleton plan"));
15655
- console.log(" " + paint("accent", "/handoff") + chalk15.dim(" export this analysis for another tool"));
16927
+ console.log(" " + chalk17.yellow("Still no engine connected \u2014 Q&A stays offline until you run ") + paint("accent", "/connect") + chalk17.yellow("."));
16928
+ console.log(" " + chalk17.dim("These work without one:"));
16929
+ console.log(" " + paint("accent", "/deepdive") + chalk17.dim(" metric slides \u2014 what each number means"));
16930
+ console.log(" " + paint("accent", "/playbook") + chalk17.dim(" recommended plays from your computed vitals"));
16931
+ console.log(" " + chalk17.cyan('"how should we fix this?"') + chalk17.dim(" deterministic skeleton plan"));
16932
+ console.log(" " + paint("accent", "/handoff") + chalk17.dim(" export this analysis for another tool"));
15656
16933
  console.log();
15657
16934
  recordMessage(
15658
16935
  ctx,
15659
16936
  "agent",
15660
- "No LLM engine connected \u2014 offered keyless paths (/playbook, skeleton plan, /handoff)."
16937
+ "No LLM engine connected \u2014 offered keyless paths (/deepdive, /playbook, skeleton plan, /handoff)."
15661
16938
  );
15662
16939
  }
15663
16940
  var NO_KEY_NUDGES;
@@ -15665,7 +16942,7 @@ var init_orchestrator = __esm({
15665
16942
  "src/conversation/orchestrator.ts"() {
15666
16943
  "use strict";
15667
16944
  init_context2();
15668
- init_store();
16945
+ init_exports_registry();
15669
16946
  init_theme();
15670
16947
  init_phase();
15671
16948
  init_scope();
@@ -15677,6 +16954,7 @@ var init_orchestrator = __esm({
15677
16954
  init_time_bank();
15678
16955
  init_pending_ask();
15679
16956
  init_keyless_ask();
16957
+ init_keyless_definitions();
15680
16958
  NO_KEY_NUDGES = /* @__PURE__ */ Symbol.for("ntrp.noKeyNudges");
15681
16959
  }
15682
16960
  });
@@ -15842,8 +17120,8 @@ var init_bundle = __esm({
15842
17120
  });
15843
17121
 
15844
17122
  // src/repositories/markdown.ts
15845
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync14 } from "fs";
15846
- import { basename as basename3, dirname as dirname2, join as join20, resolve as resolve6 } from "path";
17123
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
17124
+ import { basename as basename4, dirname as dirname3, join as join20, resolve as resolve8 } from "path";
15847
17125
  import { stringify as stringifyYaml2 } from "yaml";
15848
17126
  function renderMarkdownFiles(pkg) {
15849
17127
  const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
@@ -16030,10 +17308,10 @@ function renderStrategy(entry) {
16030
17308
  ].join("\n");
16031
17309
  }
16032
17310
  function getRootPath(target) {
16033
- return resolve6(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
17311
+ return resolve8(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
16034
17312
  }
16035
17313
  function safeFilename(value) {
16036
- return (basename3(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
17314
+ return (basename4(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
16037
17315
  }
16038
17316
  function escapeSummary(value) {
16039
17317
  return value.replace(/[<>]/g, "");
@@ -16046,7 +17324,7 @@ var init_markdown2 = __esm({
16046
17324
  markdownRepositoryAdapter = {
16047
17325
  kind: "markdown",
16048
17326
  describeTarget(target) {
16049
- return target.directory ? `local markdown folder ${resolve6(target.directory)}` : "local markdown folder";
17327
+ return target.directory ? `local markdown folder ${resolve8(target.directory)}` : "local markdown folder";
16050
17328
  },
16051
17329
  planWrite(pkg) {
16052
17330
  const files = renderMarkdownFiles(pkg);
@@ -16063,12 +17341,12 @@ var init_markdown2 = __esm({
16063
17341
  write(pkg) {
16064
17342
  const root = getRootPath(pkg.target);
16065
17343
  const files = renderMarkdownFiles(pkg);
16066
- mkdirSync8(root, { recursive: true });
17344
+ mkdirSync9(root, { recursive: true });
16067
17345
  const written = [];
16068
17346
  for (const file of files) {
16069
17347
  const absolutePath = join20(root, file.relativePath);
16070
- mkdirSync8(dirname2(absolutePath), { recursive: true });
16071
- writeFileSync14(absolutePath, file.contents, "utf-8");
17348
+ mkdirSync9(dirname3(absolutePath), { recursive: true });
17349
+ writeFileSync15(absolutePath, file.contents, "utf-8");
16072
17350
  written.push(absolutePath);
16073
17351
  }
16074
17352
  return {
@@ -16304,7 +17582,7 @@ var nl_exports = {};
16304
17582
  __export(nl_exports, {
16305
17583
  runNaturalLanguage: () => runNaturalLanguage
16306
17584
  });
16307
- import chalk16 from "chalk";
17585
+ import chalk18 from "chalk";
16308
17586
  async function runNaturalLanguage(input, ctx) {
16309
17587
  if (isSmokeProtocolTrigger(input)) {
16310
17588
  recordMessage(ctx, "user", input);
@@ -16319,7 +17597,7 @@ async function runNaturalLanguage(input, ctx) {
16319
17597
  return extractSummary(result.answer);
16320
17598
  } catch (err) {
16321
17599
  spinner2.fail("Smoke protocol failed");
16322
- console.error(" " + chalk16.red(String(err.message ?? err)));
17600
+ console.error(" " + chalk18.red(String(err.message ?? err)));
16323
17601
  console.log();
16324
17602
  return;
16325
17603
  }
@@ -16349,8 +17627,8 @@ async function runNaturalLanguage(input, ctx) {
16349
17627
  spinner2.succeed(metricsFirst ? "Session context ready" : "Health snapshot ready");
16350
17628
  } catch (err) {
16351
17629
  spinner2.fail("Could not compute health snapshot");
16352
- console.error(" " + chalk16.red(String(err.message ?? err)));
16353
- console.log(" " + chalk16.dim("Run ") + paint("accent", "/new") + chalk16.dim(" \u2192 pick Demo to load sample data."));
17630
+ console.error(" " + chalk18.red(String(err.message ?? err)));
17631
+ console.log(" " + chalk18.dim("Run ") + paint("accent", "/new") + chalk18.dim(" \u2192 pick Demo to load sample data."));
16354
17632
  console.log();
16355
17633
  return;
16356
17634
  }
@@ -16388,7 +17666,7 @@ async function runNaturalLanguage(input, ctx) {
16388
17666
  break;
16389
17667
  case "thinking":
16390
17668
  spinner.stop();
16391
- console.log(" " + chalk16.dim.italic(event.text));
17669
+ console.log(" " + chalk18.dim.italic(event.text));
16392
17670
  spinner.start("Thinking\u2026");
16393
17671
  break;
16394
17672
  case "answer":
@@ -16408,7 +17686,7 @@ async function runNaturalLanguage(input, ctx) {
16408
17686
  }
16409
17687
  } catch (err) {
16410
17688
  spinner.fail("Error while investigating");
16411
- console.error(" " + chalk16.red(String(err.message ?? err)));
17689
+ console.error(" " + chalk18.red(String(err.message ?? err)));
16412
17690
  console.log();
16413
17691
  return;
16414
17692
  } finally {
@@ -16418,7 +17696,7 @@ async function runNaturalLanguage(input, ctx) {
16418
17696
  ctx.conversation = distillThread(rawHistory);
16419
17697
  }
16420
17698
  if (!lastAnswer) {
16421
- console.log(" " + chalk16.dim("(no answer returned)"));
17699
+ console.log(" " + chalk18.dim("(no answer returned)"));
16422
17700
  } else {
16423
17701
  recordMessage(ctx, "agent", lastAnswer);
16424
17702
  if (ctx.pendingAsk) {
@@ -16450,10 +17728,15 @@ function extractSummary(text) {
16450
17728
  function printFindingInline(finding) {
16451
17729
  const sev = finding.severity;
16452
17730
  console.log();
16453
- console.log(" " + severityPaint(sev)(`[${sev}]`) + " " + chalk16.bold(finding.segment));
17731
+ console.log(" " + severityPaint(sev)(`[${sev}]`) + " " + chalk18.bold(finding.segment));
16454
17732
  printMarkdown(finding.finding, { indent: 2 });
16455
17733
  const play = finding.recommended_plays?.[0];
16456
- if (play) console.log(" " + chalk16.dim("\u2192 " + play.play_name + " \u2014 " + play.rationale));
17734
+ if (play) console.log(" " + chalk18.dim("\u2192 " + play.play_name + " \u2014 " + play.rationale));
17735
+ if (finding.recommended_focus) {
17736
+ console.log(
17737
+ " " + chalk18.dim("How this works: ") + paint("accent", `/deepdive ${finding.recommended_focus}`)
17738
+ );
17739
+ }
16457
17740
  }
16458
17741
  var init_nl = __esm({
16459
17742
  "src/cli/nl.ts"() {
@@ -16487,12 +17770,12 @@ __export(demo_exports, {
16487
17770
  printDemoDisabled: () => printDemoDisabled,
16488
17771
  setDemoEnabled: () => setDemoEnabled
16489
17772
  });
16490
- import chalk17 from "chalk";
17773
+ import chalk19 from "chalk";
16491
17774
  function printDemoDisabled() {
16492
17775
  console.log();
16493
- console.log(" " + chalk17.red(DEMO_DISABLED_MESSAGE));
17776
+ console.log(" " + chalk19.red(DEMO_DISABLED_MESSAGE));
16494
17777
  console.log(
16495
- " " + chalk17.dim("Re-enable with ") + paint("accent", "/config set demo-enabled true") + chalk17.dim(".")
17778
+ " " + chalk19.dim("Re-enable with ") + paint("accent", "/config set demo-enabled true") + chalk19.dim(".")
16496
17779
  );
16497
17780
  console.log();
16498
17781
  }
@@ -16534,7 +17817,7 @@ __export(pending_ask_exports, {
16534
17817
  queuePendingAsk: () => queuePendingAsk,
16535
17818
  resumePendingAsk: () => resumePendingAsk
16536
17819
  });
16537
- import chalk18 from "chalk";
17820
+ import chalk20 from "chalk";
16538
17821
  function looksLikeQuestion(input) {
16539
17822
  const text = input.trim();
16540
17823
  if (!text) return false;
@@ -16570,7 +17853,7 @@ function printFocusChip(ctx) {
16570
17853
  const period = ctx.scope.time_horizon ? ` \xB7 ${ctx.scope.time_horizon}` : "";
16571
17854
  console.log();
16572
17855
  console.log(
16573
- " " + chalk18.dim("Focus: ") + paint("accent", lens) + chalk18.dim(period) + chalk18.dim(" \u2014 type ") + chalk18.cyan("adjust") + chalk18.dim(" to change")
17856
+ " " + chalk20.dim("Focus: ") + paint("accent", lens) + chalk20.dim(period) + chalk20.dim(" \u2014 type ") + chalk20.cyan("adjust") + chalk20.dim(" to change")
16574
17857
  );
16575
17858
  console.log();
16576
17859
  }
@@ -16581,7 +17864,7 @@ async function resumePendingAsk(ctx) {
16581
17864
  if (canUseReplAi(ctx)) {
16582
17865
  console.log();
16583
17866
  console.log(
16584
- " " + chalk18.dim(
17867
+ " " + chalk20.dim(
16585
17868
  pending.keylessAnswered ? "Picking up your question with the connected engine\u2026" : "Picking up your question\u2026"
16586
17869
  )
16587
17870
  );
@@ -16614,7 +17897,7 @@ async function offerDemoToAnswer(ctx) {
16614
17897
  const go = await prompts.confirm("Use demo data to answer this?", true);
16615
17898
  if (!go) {
16616
17899
  console.log(
16617
- " " + chalk18.dim("Paste a CSV path when ready, or say ") + chalk18.cyan("use demo data") + chalk18.dim(".")
17900
+ " " + chalk20.dim("Paste a CSV path when ready, or say ") + chalk20.cyan("use demo data") + chalk20.dim(".")
16618
17901
  );
16619
17902
  console.log();
16620
17903
  return false;
@@ -16646,7 +17929,7 @@ __export(compute_exports2, {
16646
17929
  isComputeIntent: () => isComputeIntent,
16647
17930
  runConversationCompute: () => runConversationCompute
16648
17931
  });
16649
- import chalk19 from "chalk";
17932
+ import chalk21 from "chalk";
16650
17933
  async function runConversationCompute(ctx) {
16651
17934
  const lens = ctx.scope?.primary_lens ?? ctx.analysis.primary;
16652
17935
  ctx.computeInProgress = true;
@@ -16701,7 +17984,7 @@ async function runConversationCompute(ctx) {
16701
17984
  creditGapCompute(ctx);
16702
17985
  return typeof summary === "string" ? summary : "Health analysis ready";
16703
17986
  } catch (err) {
16704
- console.error(" " + chalk19.red(String(err.message ?? err)));
17987
+ console.error(" " + chalk21.red(String(err.message ?? err)));
16705
17988
  return;
16706
17989
  } finally {
16707
17990
  ctx.computeInProgress = false;
@@ -19470,18 +20753,18 @@ var init_generator = __esm({
19470
20753
  });
19471
20754
 
19472
20755
  // src/demo/taxonomy-cache.ts
19473
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync15, existsSync as existsSync18, mkdirSync as mkdirSync9, unlinkSync as unlinkSync3 } from "fs";
19474
- import { homedir as homedir5 } from "os";
20756
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync16, existsSync as existsSync19, mkdirSync as mkdirSync10, unlinkSync as unlinkSync3 } from "fs";
20757
+ import { homedir as homedir6 } from "os";
19475
20758
  import { join as join22 } from "path";
19476
20759
  function ensureDir5() {
19477
- if (!existsSync18(NTRP_DIR4)) {
19478
- mkdirSync9(NTRP_DIR4, { recursive: true });
20760
+ if (!existsSync19(NTRP_DIR4)) {
20761
+ mkdirSync10(NTRP_DIR4, { recursive: true });
19479
20762
  }
19480
20763
  }
19481
20764
  function loadCachedTaxonomy(profile) {
19482
- if (!existsSync18(TAXONOMY_PATH)) return null;
20765
+ if (!existsSync19(TAXONOMY_PATH)) return null;
19483
20766
  try {
19484
- const parsed = JSON.parse(readFileSync17(TAXONOMY_PATH, "utf-8"));
20767
+ const parsed = JSON.parse(readFileSync18(TAXONOMY_PATH, "utf-8"));
19485
20768
  if (!parsed || typeof parsed !== "object") return null;
19486
20769
  if (parsed.profile_updated_at !== profile.updated_at) return null;
19487
20770
  return parsed;
@@ -19491,13 +20774,13 @@ function loadCachedTaxonomy(profile) {
19491
20774
  }
19492
20775
  function saveCachedTaxonomy(taxonomy) {
19493
20776
  ensureDir5();
19494
- writeFileSync15(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
20777
+ writeFileSync16(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
19495
20778
  }
19496
20779
  var NTRP_DIR4, TAXONOMY_PATH;
19497
20780
  var init_taxonomy_cache = __esm({
19498
20781
  "src/demo/taxonomy-cache.ts"() {
19499
20782
  "use strict";
19500
- NTRP_DIR4 = join22(homedir5(), ".ntrp");
20783
+ NTRP_DIR4 = join22(homedir6(), ".ntrp");
19501
20784
  TAXONOMY_PATH = join22(NTRP_DIR4, "demo-taxonomy.json");
19502
20785
  }
19503
20786
  });
@@ -19734,16 +21017,16 @@ var generate_exports = {};
19734
21017
  __export(generate_exports, {
19735
21018
  handler: () => handler2
19736
21019
  });
19737
- import chalk20 from "chalk";
21020
+ import chalk22 from "chalk";
19738
21021
  async function handler2(args, ctx) {
19739
21022
  const { flags } = parseArgs(args, ["list-scenarios", "regen-taxonomy", "brief"]);
19740
21023
  const quiet = ctx.execution.quiet;
19741
21024
  const brief = getBool(flags, "brief");
19742
21025
  if (getBool(flags, "list-scenarios")) {
19743
- console.log(chalk20.bold("\n Available Scenarios:\n"));
21026
+ console.log(chalk22.bold("\n Available Scenarios:\n"));
19744
21027
  for (const s of SCENARIO_LIST) {
19745
- console.log(` ${chalk20.cyan(s.key.padEnd(20))} ${s.label}`);
19746
- console.log(` ${chalk20.dim(" ".repeat(20))} ${s.description}
21028
+ console.log(` ${chalk22.cyan(s.key.padEnd(20))} ${s.label}`);
21029
+ console.log(` ${chalk22.dim(" ".repeat(20))} ${s.description}
19747
21030
  `);
19748
21031
  }
19749
21032
  return true;
@@ -19753,9 +21036,9 @@ async function handler2(args, ctx) {
19753
21036
  const skipProfile = getFalse(flags, "profile");
19754
21037
  if (!isProfileConfigured(profile) && !skipProfile) {
19755
21038
  console.error();
19756
- console.error(" " + chalk20.red("No company profile found."));
19757
- console.error(" " + chalk20.dim("Run ") + paint("accent", "/onboard") + chalk20.dim(" first for a richer demo,"));
19758
- console.error(" " + chalk20.dim("or pass ") + paint("accent", "--no-profile") + chalk20.dim(" to skip."));
21039
+ console.error(" " + chalk22.red("No company profile found."));
21040
+ console.error(" " + chalk22.dim("Run ") + paint("accent", "/onboard") + chalk22.dim(" first for a richer demo,"));
21041
+ console.error(" " + chalk22.dim("or pass ") + paint("accent", "--no-profile") + chalk22.dim(" to skip."));
19759
21042
  console.error();
19760
21043
  markFailure(ctx);
19761
21044
  return false;
@@ -19763,8 +21046,8 @@ async function handler2(args, ctx) {
19763
21046
  const explicitScenario = getString(flags, "scenario", "s");
19764
21047
  const resolvedScenario = resolveScenarioInput(explicitScenario);
19765
21048
  if (resolvedScenario === null) {
19766
- console.error(chalk20.red(` Unknown scenario: ${explicitScenario}`));
19767
- console.log(chalk20.dim(` Valid: ${SCENARIO_LIST.map((s) => s.key).join(", ")}`));
21049
+ console.error(chalk22.red(` Unknown scenario: ${explicitScenario}`));
21050
+ console.log(chalk22.dim(` Valid: ${SCENARIO_LIST.map((s) => s.key).join(", ")}`));
19768
21051
  markFailure(ctx);
19769
21052
  return false;
19770
21053
  }
@@ -19778,10 +21061,10 @@ async function handler2(args, ctx) {
19778
21061
  const s = getScenario(scenario);
19779
21062
  console.log();
19780
21063
  if (brief) {
19781
- console.log(" " + paint("accent", "\u2713 Demo: ") + bold(s.label) + chalk20.dim(" \u2014 " + s.hook));
21064
+ console.log(" " + paint("accent", "\u2713 Demo: ") + bold(s.label) + chalk22.dim(" \u2014 " + s.hook));
19782
21065
  } else {
19783
21066
  console.log(" " + paint("accent", "\u2713 Scenario: ") + bold(s.label));
19784
- console.log(" " + chalk20.dim(s.story));
21067
+ console.log(" " + chalk22.dim(s.story));
19785
21068
  console.log();
19786
21069
  }
19787
21070
  }
@@ -19811,18 +21094,18 @@ async function handler2(args, ctx) {
19811
21094
  if (brief) {
19812
21095
  spinner.succeed(`Demo loaded \u2014 ${briefCounts(result.counts)}`);
19813
21096
  } else {
19814
- spinner.succeed(`Generated demo data for "${chalk20.cyan(scenario)}" scenario`);
21097
+ spinner.succeed(`Generated demo data for "${chalk22.cyan(scenario)}" scenario`);
19815
21098
  console.log();
19816
21099
  printEntityCounts(result.counts);
19817
21100
  }
19818
21101
  }
19819
21102
  if (!quiet && !brief && ctx.analysis.primary !== "revenue_metrics") {
19820
- console.log(chalk20.dim("\n Run /diagnose to compute vital signs.\n"));
21103
+ console.log(chalk22.dim("\n Run /diagnose to compute vital signs.\n"));
19821
21104
  }
19822
21105
  }
19823
21106
  } catch (err) {
19824
21107
  if (spinner) spinner.fail("Generation failed");
19825
- console.error(chalk20.red(String(err)));
21108
+ console.error(chalk22.red(String(err)));
19826
21109
  markFailure(ctx);
19827
21110
  return false;
19828
21111
  }
@@ -19860,7 +21143,7 @@ async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
19860
21143
  return taxonomy;
19861
21144
  } catch (err) {
19862
21145
  spinner.fail("Couldn't build market taxonomy \u2014 using generic data pools");
19863
- console.log(" " + chalk20.dim(String(err.message ?? err)));
21146
+ console.log(" " + chalk22.dim(String(err.message ?? err)));
19864
21147
  return void 0;
19865
21148
  }
19866
21149
  }
@@ -19951,9 +21234,9 @@ var ingest_exports = {};
19951
21234
  __export(ingest_exports, {
19952
21235
  handler: () => handler3
19953
21236
  });
19954
- import chalk21 from "chalk";
19955
- import { readFileSync as readFileSync18, existsSync as existsSync19 } from "fs";
19956
- import { basename as basename4 } from "path";
21237
+ import chalk23 from "chalk";
21238
+ import { readFileSync as readFileSync19, existsSync as existsSync20 } from "fs";
21239
+ import { basename as basename5 } from "path";
19957
21240
  async function handler3(args, ctx) {
19958
21241
  const { positional, flags } = parseArgs(args, [
19959
21242
  "skip-resolve",
@@ -19972,21 +21255,21 @@ async function handler3(args, ctx) {
19972
21255
  const source = getString(flags, "source", "s") ?? "salesforce";
19973
21256
  const skipResolve = getBool(flags, "skip-resolve");
19974
21257
  if (!file) {
19975
- console.error(chalk21.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
19976
- console.error(chalk21.dim(" /ingest --demo [--scenario <name>]"));
21258
+ console.error(chalk23.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
21259
+ console.error(chalk23.dim(" /ingest --demo [--scenario <name>]"));
19977
21260
  process.exit(1);
19978
21261
  }
19979
- if (!existsSync19(file)) {
19980
- console.error(chalk21.red(` File not found: ${file}`));
21262
+ if (!existsSync20(file)) {
21263
+ console.error(chalk23.red(` File not found: ${file}`));
19981
21264
  process.exit(1);
19982
21265
  }
19983
21266
  const profile = loadProfile();
19984
21267
  const skipProfile = getFalse(flags, "profile");
19985
21268
  if (!profile && !skipProfile) {
19986
21269
  console.error();
19987
- console.error(" " + chalk21.red("No company profile found."));
19988
- console.error(" " + chalk21.dim("Run ") + paint("accent", "/onboard") + chalk21.dim(" first for better column mapping,"));
19989
- console.error(" " + chalk21.dim("or pass ") + paint("accent", "--no-profile") + chalk21.dim(" to skip."));
21270
+ console.error(" " + chalk23.red("No company profile found."));
21271
+ console.error(" " + chalk23.dim("Run ") + paint("accent", "/onboard") + chalk23.dim(" first for better column mapping,"));
21272
+ console.error(" " + chalk23.dim("or pass ") + paint("accent", "--no-profile") + chalk23.dim(" to skip."));
19990
21273
  console.error();
19991
21274
  process.exit(1);
19992
21275
  }
@@ -19994,7 +21277,7 @@ async function handler3(args, ctx) {
19994
21277
  try {
19995
21278
  await initSchema();
19996
21279
  spinner.text = "Parsing CSV\u2026";
19997
- const content = readFileSync18(file, "utf-8");
21280
+ const content = readFileSync19(file, "utf-8");
19998
21281
  const { rows, headers } = parseCSV(content);
19999
21282
  if (rows.length === 0) {
20000
21283
  spinner.fail("CSV is empty");
@@ -20006,7 +21289,7 @@ async function handler3(args, ctx) {
20006
21289
  const { importRevenueRows: importRevenueRows2 } = await Promise.resolve().then(() => (init_revenue_importer(), revenue_importer_exports));
20007
21290
  const uploadId2 = await insertCSVUpload({
20008
21291
  source_system: source,
20009
- original_filename: basename4(file),
21292
+ original_filename: basename5(file),
20010
21293
  row_count: rows.length,
20011
21294
  column_mappings: { entity_type: "revenue_ledger" },
20012
21295
  status: "processing"
@@ -20018,28 +21301,28 @@ async function handler3(args, ctx) {
20018
21301
  row_count: result2.imported
20019
21302
  });
20020
21303
  spinner.succeed(
20021
- `Imported ${chalk21.bold(result2.imported.toString())} revenue events from ${chalk21.dim(basename4(file))}`
21304
+ `Imported ${chalk23.bold(result2.imported.toString())} revenue events from ${chalk23.dim(basename5(file))}`
20022
21305
  );
20023
21306
  if (result2.errors.length > 0) {
20024
- console.log(chalk21.yellow(` ${result2.errors.length} rows skipped`));
21307
+ console.log(chalk23.yellow(` ${result2.errors.length} rows skipped`));
20025
21308
  }
20026
21309
  if (ctx.analysis) {
20027
21310
  ctx.analysis.data_source_type = "revenue_ledger";
20028
21311
  }
20029
- console.log(chalk21.dim(" Run ") + chalk21.cyan("/metrics") + chalk21.dim(" for SaaS metrics with ledger-backed retention."));
20030
- return `${result2.imported} revenue events from ${basename4(file)}`;
21312
+ console.log(chalk23.dim(" Run ") + chalk23.cyan("/metrics") + chalk23.dim(" for SaaS metrics with ledger-backed retention."));
21313
+ return `${result2.imported} revenue events from ${basename5(file)}`;
20031
21314
  }
20032
21315
  spinner.text = "Detecting entity type\u2026";
20033
21316
  const detection = detectEntityType(headers, source);
20034
21317
  if (!detection) {
20035
21318
  spinner.fail(`Could not auto-detect entity type for source: ${source}`);
20036
- console.log(chalk21.dim(" Headers found: " + headers.join(", ")));
21319
+ console.log(chalk23.dim(" Headers found: " + headers.join(", ")));
20037
21320
  process.exit(1);
20038
21321
  }
20039
21322
  spinner.text = `Importing ${rows.length} ${detection.entityType} rows...`;
20040
21323
  const uploadId = await insertCSVUpload({
20041
21324
  source_system: source,
20042
- original_filename: basename4(file),
21325
+ original_filename: basename5(file),
20043
21326
  row_count: rows.length,
20044
21327
  column_mappings: detection.mappings,
20045
21328
  status: "processing"
@@ -20056,15 +21339,15 @@ async function handler3(args, ctx) {
20056
21339
  row_count: result.imported
20057
21340
  });
20058
21341
  spinner.succeed(
20059
- `Imported ${chalk21.bold(result.imported.toString())} ${detection.entityType} from ${chalk21.dim(basename4(file))} (${source})`
21342
+ `Imported ${chalk23.bold(result.imported.toString())} ${detection.entityType} from ${chalk23.dim(basename5(file))} (${source})`
20060
21343
  );
20061
21344
  if (result.errors.length > 0) {
20062
- console.log(chalk21.yellow(` ${result.errors.length} rows skipped`));
21345
+ console.log(chalk23.yellow(` ${result.errors.length} rows skipped`));
20063
21346
  for (const err of result.errors.slice(0, 3)) {
20064
- console.log(chalk21.dim(` - ${err}`));
21347
+ console.log(chalk23.dim(` - ${err}`));
20065
21348
  }
20066
21349
  if (result.errors.length > 3) {
20067
- console.log(chalk21.dim(` ... and ${result.errors.length - 3} more`));
21350
+ console.log(chalk23.dim(` ... and ${result.errors.length - 3} more`));
20068
21351
  }
20069
21352
  }
20070
21353
  if (!skipResolve) {
@@ -20078,10 +21361,10 @@ async function handler3(args, ctx) {
20078
21361
  resolveSpinner.succeed("No duplicates found");
20079
21362
  }
20080
21363
  }
20081
- return `${result.imported} ${detection.entityType} from ${basename4(file)}`;
21364
+ return `${result.imported} ${detection.entityType} from ${basename5(file)}`;
20082
21365
  } catch (err) {
20083
21366
  spinner.fail("Import failed");
20084
- console.error(chalk21.red(String(err)));
21367
+ console.error(chalk23.red(String(err)));
20085
21368
  process.exit(1);
20086
21369
  }
20087
21370
  }
@@ -20111,10 +21394,10 @@ __export(ingest_chat_exports, {
20111
21394
  loadDemoFromChat: () => loadDemoFromChat,
20112
21395
  looksLikeFilePath: () => looksLikeFilePath
20113
21396
  });
20114
- import { existsSync as existsSync20 } from "fs";
20115
- import { basename as basename5, resolve as resolve7 } from "path";
20116
- import { homedir as homedir6 } from "os";
20117
- import chalk22 from "chalk";
21397
+ import { existsSync as existsSync21 } from "fs";
21398
+ import { basename as basename6, resolve as resolve9 } from "path";
21399
+ import { homedir as homedir7 } from "os";
21400
+ import chalk24 from "chalk";
20118
21401
  function extractFilePath(input) {
20119
21402
  const trimmed = input.trim();
20120
21403
  const patterns = [
@@ -20131,33 +21414,33 @@ function extractFilePath(input) {
20131
21414
  const m = trimmed.match(re);
20132
21415
  if (m?.[1]) {
20133
21416
  const p = expandPath(m[1]);
20134
- if (existsSync20(p)) return p;
21417
+ if (existsSync21(p)) return p;
20135
21418
  }
20136
21419
  if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
20137
21420
  const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
20138
- if (existsSync20(p)) return p;
21421
+ if (existsSync21(p)) return p;
20139
21422
  }
20140
21423
  }
20141
21424
  return null;
20142
21425
  }
20143
21426
  function expandPath(p) {
20144
- if (p.startsWith("~/")) return resolve7(homedir6(), p.slice(2));
20145
- return resolve7(p);
21427
+ if (p.startsWith("~/")) return resolve9(homedir7(), p.slice(2));
21428
+ return resolve9(p);
20146
21429
  }
20147
21430
  function looksLikeFilePath(input) {
20148
21431
  return extractFilePath(input) !== null;
20149
21432
  }
20150
21433
  async function ingestFromChat(ctx, filePath) {
20151
21434
  if (!ctx.rl) {
20152
- console.log(" " + chalk22.red("Ingest confirm requires interactive mode."));
21435
+ console.log(" " + chalk24.red("Ingest confirm requires interactive mode."));
20153
21436
  return false;
20154
21437
  }
20155
- const name = basename5(filePath);
21438
+ const name = basename6(filePath);
20156
21439
  const prompts = createPromptSession(ctx.rl, ctx);
20157
21440
  try {
20158
21441
  const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
20159
21442
  if (!ok) {
20160
- console.log(" " + chalk22.dim("Ingest cancelled."));
21443
+ console.log(" " + chalk24.dim("Ingest cancelled."));
20161
21444
  return false;
20162
21445
  }
20163
21446
  } finally {
@@ -20165,12 +21448,12 @@ async function ingestFromChat(ctx, filePath) {
20165
21448
  }
20166
21449
  const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
20167
21450
  const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
20168
- const { readFileSync: readFileSync19 } = await import("fs");
21451
+ const { readFileSync: readFileSync20 } = await import("fs");
20169
21452
  const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
20170
21453
  const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
20171
21454
  let headerCheckFailed = false;
20172
21455
  try {
20173
- const raw = readFileSync19(filePath, "utf-8");
21456
+ const raw = readFileSync20(filePath, "utf-8");
20174
21457
  const { headers } = parseCSV2(raw);
20175
21458
  const detected = detectEntityType2(headers, "unknown");
20176
21459
  if (!detected) headerCheckFailed = true;
@@ -20185,7 +21468,7 @@ async function ingestFromChat(ctx, filePath) {
20185
21468
  false
20186
21469
  );
20187
21470
  if (useAi) {
20188
- console.log(" " + chalk22.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
21471
+ console.log(" " + chalk24.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
20189
21472
  }
20190
21473
  } finally {
20191
21474
  prompts2.close();
@@ -20212,7 +21495,7 @@ async function ingestFromChat(ctx, filePath) {
20212
21495
  invalidateGapAudit(ctx);
20213
21496
  saveSessionState(ctx);
20214
21497
  console.log();
20215
- console.log(" " + paint("accent", "\u2713 Data loaded") + chalk22.dim(` \u2014 ${name}`));
21498
+ console.log(" " + paint("accent", "\u2713 Data loaded") + chalk24.dim(` \u2014 ${name}`));
20216
21499
  recordMessage(ctx, "user", `[ingested ${name}]`);
20217
21500
  recordMessage(ctx, "agent", `Loaded ${name}. Checking what we can analyze\u2026`);
20218
21501
  const audit = await refreshGapAudit(ctx);
@@ -20220,7 +21503,7 @@ async function ingestFromChat(ctx, filePath) {
20220
21503
  if (audit.can_compute && ctx.scope?.confirmed_at) {
20221
21504
  if (ctx.pendingAsk) {
20222
21505
  console.log();
20223
- console.log(" " + chalk22.dim("Computing so I can answer\u2026"));
21506
+ console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
20224
21507
  await runConversationCompute(ctx);
20225
21508
  return true;
20226
21509
  }
@@ -20282,7 +21565,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
20282
21565
  const shouldAuto = opts.autoCompute || Boolean(ctx.pendingAsk && audit.can_compute && ctx.scope?.confirmed_at);
20283
21566
  if (shouldAuto && audit.can_compute) {
20284
21567
  console.log();
20285
- console.log(" " + chalk22.dim("Computing so I can answer\u2026"));
21568
+ console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
20286
21569
  await runConversationCompute(ctx);
20287
21570
  return true;
20288
21571
  }
@@ -20771,9 +22054,9 @@ async function handleGetSessionBrief(input) {
20771
22054
  if (!target) {
20772
22055
  return { error: `No session matching "${raw}".` };
20773
22056
  }
20774
- const { existsSync: existsSync21, readFileSync: readFileSync19 } = await import("fs");
22057
+ const { existsSync: existsSync22, readFileSync: readFileSync20 } = await import("fs");
20775
22058
  const briefPath = contextDocPathForSession2(target.id);
20776
- if (!existsSync21(briefPath)) {
22059
+ if (!existsSync22(briefPath)) {
20777
22060
  return {
20778
22061
  session_id: target.id,
20779
22062
  error: "No context brief on disk for this session (created before brief storage existed).",
@@ -20781,7 +22064,7 @@ async function handleGetSessionBrief(input) {
20781
22064
  stage: target.stage ?? null
20782
22065
  };
20783
22066
  }
20784
- return { session_id: target.id, brief: readFileSync19(briefPath, "utf-8") };
22067
+ return { session_id: target.id, brief: readFileSync20(briefPath, "utf-8") };
20785
22068
  }
20786
22069
  function auditDenied(name, input, resultJson, start) {
20787
22070
  logToolCall({