@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.
- package/dist/ai/guardrails-smoke.js +1696 -413
- package/dist/ai/guardrails-smoke.js.map +1 -1
- package/dist/conversation/deepdive-smoke.js +3010 -0
- package/dist/conversation/deepdive-smoke.js.map +1 -0
- package/dist/conversation/loop-guard-smoke.js +4116 -1401
- package/dist/conversation/loop-guard-smoke.js.map +1 -1
- package/dist/index.js +4356 -1576
- package/dist/index.js.map +1 -1
- package/dist/investigation/quality-eval-cli.js +2058 -775
- package/dist/investigation/quality-eval-cli.js.map +1 -1
- package/dist/investigation/verbosity-cli.js +2064 -781
- package/dist/investigation/verbosity-cli.js.map +1 -1
- package/dist/mcp/server.js +2058 -774
- package/dist/mcp/server.js.map +1 -1
- package/dist/services/exports-registry-smoke.js +858 -0
- package/dist/services/exports-registry-smoke.js.map +1 -0
- package/dist/services/transcript-smoke.js +170 -34
- package/dist/services/transcript-smoke.js.map +1 -1
- package/dist/strategist/strategist-smoke.js +970 -65
- package/dist/strategist/strategist-smoke.js.map +1 -1
- package/dist/whimsy/time-bank-smoke.js +4065 -1446
- package/dist/whimsy/time-bank-smoke.js.map +1 -1
- package/package.json +3 -1
|
@@ -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
|
-
|
|
760
|
+
writeFileSync3(contextDocPathForSession(ctx.sessionId), doc);
|
|
493
761
|
} catch {
|
|
494
762
|
}
|
|
495
763
|
}
|
|
496
764
|
function writeContextDocForSessionFile(file, opts = {}) {
|
|
497
765
|
try {
|
|
498
|
-
|
|
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
|
|
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 =
|
|
519
|
-
if (
|
|
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
|
-
|
|
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 (
|
|
810
|
+
if (existsSync3(filePath)) {
|
|
541
811
|
try {
|
|
542
|
-
base =
|
|
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
|
-
|
|
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
|
|
642
|
-
import { dirname, join as
|
|
643
|
-
function
|
|
644
|
-
const dir =
|
|
645
|
-
if (!
|
|
646
|
-
|
|
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 =
|
|
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
|
-
|
|
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((
|
|
962
|
+
return new Promise((resolve11) => {
|
|
693
963
|
try {
|
|
694
|
-
close2.call(c, () =>
|
|
964
|
+
close2.call(c, () => resolve11());
|
|
695
965
|
} catch {
|
|
696
|
-
|
|
966
|
+
resolve11();
|
|
697
967
|
}
|
|
698
968
|
});
|
|
699
969
|
}
|
|
700
970
|
function isConnectionAlive(c) {
|
|
701
|
-
return new Promise((
|
|
971
|
+
return new Promise((resolve11) => {
|
|
702
972
|
try {
|
|
703
|
-
c.all("SELECT 1", (err) =>
|
|
973
|
+
c.all("SELECT 1", (err) => resolve11(!err));
|
|
704
974
|
} catch {
|
|
705
|
-
|
|
975
|
+
resolve11(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((
|
|
720
|
-
currentDb.close(() =>
|
|
989
|
+
await new Promise((resolve11) => {
|
|
990
|
+
currentDb.close(() => resolve11());
|
|
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((
|
|
1005
|
+
return new Promise((resolve11, reject) => {
|
|
736
1006
|
const cb = (err, rows) => {
|
|
737
1007
|
if (err) reject(err);
|
|
738
|
-
else
|
|
1008
|
+
else resolve11(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((
|
|
1023
|
+
return new Promise((resolve11, 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
|
|
1029
|
+
else resolve11();
|
|
760
1030
|
});
|
|
761
1031
|
} else {
|
|
762
1032
|
c.run(sql, (err) => {
|
|
763
1033
|
if (err) reject(err);
|
|
764
|
-
else
|
|
1034
|
+
else resolve11();
|
|
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
|
-
|
|
1054
|
+
rmSync3(path, { force: true });
|
|
785
1055
|
}
|
|
786
1056
|
}
|
|
787
|
-
var
|
|
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
|
-
|
|
792
|
-
DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ?
|
|
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
|
|
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/
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
|
1933
|
-
|
|
1934
|
-
|
|
2187
|
+
function ensureDir3() {
|
|
2188
|
+
const dir = ntrpHome();
|
|
2189
|
+
if (!existsSync5(dir)) {
|
|
2190
|
+
mkdirSync4(dir, { recursive: true });
|
|
1935
2191
|
}
|
|
1936
2192
|
}
|
|
1937
|
-
function
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
|
1957
|
-
|
|
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
|
|
2132
|
-
import { join as
|
|
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
|
|
2237
|
+
return join6(ntrpHome(), "state.json");
|
|
2135
2238
|
}
|
|
2136
2239
|
function legacyStateBackupPath() {
|
|
2137
|
-
return
|
|
2240
|
+
return join6(ntrpHome(), "state.json.bak");
|
|
2138
2241
|
}
|
|
2139
2242
|
function progressPath() {
|
|
2140
|
-
return
|
|
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 (
|
|
2251
|
+
if (existsSync6(progressPath())) return null;
|
|
2149
2252
|
const legacyPath = legacyStatePath();
|
|
2150
|
-
if (!
|
|
2253
|
+
if (!existsSync6(legacyPath)) return null;
|
|
2151
2254
|
try {
|
|
2152
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
2263
|
+
writeFileSync6(progressPath(), JSON.stringify(progress, null, 2) + "\n");
|
|
2161
2264
|
try {
|
|
2162
|
-
|
|
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
|
|
2268
|
-
import { join as
|
|
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
|
|
2373
|
+
return join7(ntrpHome(), "progress.json");
|
|
2271
2374
|
}
|
|
2272
2375
|
function ensureDir4() {
|
|
2273
2376
|
const dir = ntrpHome();
|
|
2274
|
-
if (!
|
|
2275
|
-
|
|
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 (!
|
|
2408
|
+
if (!existsSync7(path)) return { state: null, changed: false };
|
|
2306
2409
|
try {
|
|
2307
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
|
3115
|
-
import { join as
|
|
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
|
|
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 (!
|
|
3225
|
+
if (!existsSync8(path)) {
|
|
3123
3226
|
cachedEntries = [];
|
|
3124
3227
|
return cachedEntries;
|
|
3125
3228
|
}
|
|
3126
3229
|
try {
|
|
3127
|
-
const parsed = JSON.parse(
|
|
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
|
|
3889
|
-
import { join as
|
|
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
|
|
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 (!
|
|
3999
|
+
if (!existsSync9(path)) {
|
|
3897
4000
|
cached = { version: 1, providers: {} };
|
|
3898
4001
|
return cached;
|
|
3899
4002
|
}
|
|
3900
4003
|
try {
|
|
3901
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
|
4173
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
4071
4174
|
function fixtureResponse(url, headers) {
|
|
4072
4175
|
try {
|
|
4073
|
-
const raw =
|
|
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) {
|
|
@@ -5043,24 +5146,24 @@ var init_errors2 = __esm({
|
|
|
5043
5146
|
|
|
5044
5147
|
// src/strategies/readers.ts
|
|
5045
5148
|
import { createHash } from "crypto";
|
|
5046
|
-
import { existsSync as
|
|
5047
|
-
import { extname, resolve as
|
|
5149
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
5150
|
+
import { extname, resolve as resolve5 } from "path";
|
|
5048
5151
|
import { parse as parseYaml } from "yaml";
|
|
5049
5152
|
import { PDFParse } from "pdf-parse";
|
|
5050
5153
|
async function readStrategyFile(pathOrDash) {
|
|
5051
5154
|
if (pathOrDash === "-") {
|
|
5052
|
-
const text2 =
|
|
5155
|
+
const text2 = readFileSync10(0, "utf-8");
|
|
5053
5156
|
return createDocument("stdin", null, text2, {});
|
|
5054
5157
|
}
|
|
5055
|
-
const sourcePath =
|
|
5056
|
-
if (!
|
|
5158
|
+
const sourcePath = resolve5(pathOrDash);
|
|
5159
|
+
if (!existsSync10(sourcePath)) {
|
|
5057
5160
|
throw new NtrpError("strategy_file_not_found", `Strategy file not found: ${pathOrDash}`, 2 /* Usage */);
|
|
5058
5161
|
}
|
|
5059
5162
|
const ext = extname(sourcePath).toLowerCase();
|
|
5060
5163
|
if (ext === ".pdf") {
|
|
5061
5164
|
return readPdf(sourcePath);
|
|
5062
5165
|
}
|
|
5063
|
-
const text =
|
|
5166
|
+
const text = readFileSync10(sourcePath, "utf-8");
|
|
5064
5167
|
if (ext === ".yaml" || ext === ".yml") {
|
|
5065
5168
|
const structured = parseStructuredYaml(text);
|
|
5066
5169
|
return createDocument("yaml", sourcePath, text, structured);
|
|
@@ -5075,7 +5178,7 @@ function readStrategyText(text) {
|
|
|
5075
5178
|
return createDocument("text", null, text, {});
|
|
5076
5179
|
}
|
|
5077
5180
|
async function readPdf(sourcePath) {
|
|
5078
|
-
const data =
|
|
5181
|
+
const data = readFileSync10(sourcePath);
|
|
5079
5182
|
const parser = new PDFParse({ data });
|
|
5080
5183
|
try {
|
|
5081
5184
|
const result = await parser.getText();
|
|
@@ -5122,17 +5225,17 @@ var init_readers = __esm({
|
|
|
5122
5225
|
});
|
|
5123
5226
|
|
|
5124
5227
|
// src/memory/knowledge.ts
|
|
5125
|
-
import { existsSync as
|
|
5126
|
-
import { join as
|
|
5127
|
-
import { randomUUID as
|
|
5228
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, appendFileSync as appendFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
5229
|
+
import { join as join10 } from "path";
|
|
5230
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
5128
5231
|
function knowledgePath() {
|
|
5129
|
-
return
|
|
5232
|
+
return join10(getMemoryDir(), KNOWLEDGE_FILE);
|
|
5130
5233
|
}
|
|
5131
5234
|
function loadKnowledgeChunks() {
|
|
5132
5235
|
const path = knowledgePath();
|
|
5133
|
-
if (!
|
|
5236
|
+
if (!existsSync11(path)) return [];
|
|
5134
5237
|
const out = [];
|
|
5135
|
-
for (const line of
|
|
5238
|
+
for (const line of readFileSync11(path, "utf-8").split("\n")) {
|
|
5136
5239
|
const trimmed = line.trim();
|
|
5137
5240
|
if (!trimmed) continue;
|
|
5138
5241
|
try {
|
|
@@ -5165,16 +5268,16 @@ __export(playbook_exports, {
|
|
|
5165
5268
|
getPlaysForVitalSign: () => getPlaysForVitalSign,
|
|
5166
5269
|
matchTriggeredPlays: () => matchTriggeredPlays
|
|
5167
5270
|
});
|
|
5168
|
-
import { existsSync as
|
|
5169
|
-
import { join as
|
|
5271
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12, appendFileSync as appendFileSync3 } from "fs";
|
|
5272
|
+
import { join as join11 } from "path";
|
|
5170
5273
|
function playsPath() {
|
|
5171
|
-
return
|
|
5274
|
+
return join11(getMemoryDir(), PLAYS_FILE);
|
|
5172
5275
|
}
|
|
5173
5276
|
function getCustomPlays() {
|
|
5174
5277
|
const path = playsPath();
|
|
5175
|
-
if (!
|
|
5278
|
+
if (!existsSync12(path)) return [];
|
|
5176
5279
|
const out = [];
|
|
5177
|
-
for (const line of
|
|
5280
|
+
for (const line of readFileSync12(path, "utf-8").split("\n")) {
|
|
5178
5281
|
const trimmed = line.trim();
|
|
5179
5282
|
if (!trimmed) continue;
|
|
5180
5283
|
try {
|
|
@@ -5206,7 +5309,7 @@ function addCustomPlay(input) {
|
|
|
5206
5309
|
source: "learned"
|
|
5207
5310
|
};
|
|
5208
5311
|
try {
|
|
5209
|
-
|
|
5312
|
+
appendFileSync3(playsPath(), JSON.stringify(play) + "\n");
|
|
5210
5313
|
} catch {
|
|
5211
5314
|
}
|
|
5212
5315
|
return play;
|
|
@@ -5596,15 +5699,15 @@ JSON SHAPE:
|
|
|
5596
5699
|
});
|
|
5597
5700
|
|
|
5598
5701
|
// src/strategies/library.ts
|
|
5599
|
-
import { writeFileSync as
|
|
5600
|
-
import { join as
|
|
5702
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
5703
|
+
import { join as join12 } from "path";
|
|
5601
5704
|
import { stringify as stringifyYaml } from "yaml";
|
|
5602
5705
|
function strategyLibraryPath(slug) {
|
|
5603
|
-
return
|
|
5706
|
+
return join12(getStrategiesDir(), `${slug}.md`);
|
|
5604
5707
|
}
|
|
5605
5708
|
function writeStrategyMarkdown(strategy) {
|
|
5606
5709
|
const path = strategyLibraryPath(strategy.slug);
|
|
5607
|
-
|
|
5710
|
+
writeFileSync10(path, renderStrategyMarkdown(strategy), "utf-8");
|
|
5608
5711
|
return path;
|
|
5609
5712
|
}
|
|
5610
5713
|
function renderStrategyMarkdown(strategy) {
|
|
@@ -5728,12 +5831,12 @@ var init_library = __esm({
|
|
|
5728
5831
|
});
|
|
5729
5832
|
|
|
5730
5833
|
// src/strategies/connectors.ts
|
|
5731
|
-
import { readdirSync as
|
|
5732
|
-
import { homedir as
|
|
5733
|
-
import { basename, extname as extname2, join as
|
|
5834
|
+
import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
5835
|
+
import { homedir as homedir3 } from "os";
|
|
5836
|
+
import { basename as basename2, extname as extname2, join as join13, relative, resolve as resolve6, sep as sep3 } from "path";
|
|
5734
5837
|
function createLocalFolderConnector(options) {
|
|
5735
|
-
const rootPath =
|
|
5736
|
-
const name = options.name ?? (
|
|
5838
|
+
const rootPath = resolveUserPath2(options.rootPath);
|
|
5839
|
+
const name = options.name ?? (basename2(rootPath) || "local");
|
|
5737
5840
|
const includePatterns = normalizePatterns(options.includePatterns);
|
|
5738
5841
|
const excludePatterns = normalizePatterns(options.excludePatterns);
|
|
5739
5842
|
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
@@ -5772,8 +5875,8 @@ function createLocalFolderConnector(options) {
|
|
|
5772
5875
|
};
|
|
5773
5876
|
}
|
|
5774
5877
|
function walkLocalFolder(rootPath, currentPath, refs, opts) {
|
|
5775
|
-
for (const entry of
|
|
5776
|
-
const absolutePath =
|
|
5878
|
+
for (const entry of readdirSync3(currentPath, { withFileTypes: true })) {
|
|
5879
|
+
const absolutePath = join13(currentPath, entry.name);
|
|
5777
5880
|
const relativePath = normalizePath(relative(rootPath, absolutePath));
|
|
5778
5881
|
if (entry.isDirectory()) {
|
|
5779
5882
|
if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
|
|
@@ -5807,7 +5910,7 @@ function shouldSkipDirectory(name) {
|
|
|
5807
5910
|
}
|
|
5808
5911
|
function safeStat(path) {
|
|
5809
5912
|
try {
|
|
5810
|
-
return
|
|
5913
|
+
return statSync2(path);
|
|
5811
5914
|
} catch {
|
|
5812
5915
|
return null;
|
|
5813
5916
|
}
|
|
@@ -5821,7 +5924,7 @@ function matchesAny(relativePath, patterns) {
|
|
|
5821
5924
|
function matchesPattern(relativePath, pattern) {
|
|
5822
5925
|
const normalizedPath = normalizePath(relativePath);
|
|
5823
5926
|
const normalizedPattern = normalizePath(pattern);
|
|
5824
|
-
const base =
|
|
5927
|
+
const base = basename2(normalizedPath);
|
|
5825
5928
|
if (!normalizedPattern.includes("*")) {
|
|
5826
5929
|
return normalizedPath === normalizedPattern || normalizedPath.endsWith(`/${normalizedPattern}`) || normalizedPath.includes(normalizedPattern);
|
|
5827
5930
|
}
|
|
@@ -5833,12 +5936,12 @@ function wildcardToRegExp(pattern) {
|
|
|
5833
5936
|
return new RegExp(`^${escaped}$`, "i");
|
|
5834
5937
|
}
|
|
5835
5938
|
function normalizePath(path) {
|
|
5836
|
-
return path.split(
|
|
5939
|
+
return path.split(sep3).join("/");
|
|
5837
5940
|
}
|
|
5838
|
-
function
|
|
5839
|
-
if (path === "~") return
|
|
5840
|
-
if (path.startsWith("~/")) return
|
|
5841
|
-
return
|
|
5941
|
+
function resolveUserPath2(path) {
|
|
5942
|
+
if (path === "~") return homedir3();
|
|
5943
|
+
if (path.startsWith("~/")) return join13(homedir3(), path.slice(2));
|
|
5944
|
+
return resolve6(path);
|
|
5842
5945
|
}
|
|
5843
5946
|
var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
|
|
5844
5947
|
var init_connectors = __esm({
|
|
@@ -6049,17 +6152,17 @@ __export(store_exports2, {
|
|
|
6049
6152
|
rewriteJsonl: () => rewriteJsonl,
|
|
6050
6153
|
scrubText: () => scrubText
|
|
6051
6154
|
});
|
|
6052
|
-
import { existsSync as
|
|
6053
|
-
import { join as
|
|
6054
|
-
import { randomUUID as
|
|
6155
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync11 } from "fs";
|
|
6156
|
+
import { join as join14 } from "path";
|
|
6157
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6055
6158
|
function memPath(file) {
|
|
6056
|
-
return
|
|
6159
|
+
return join14(getMemoryDir(), file);
|
|
6057
6160
|
}
|
|
6058
6161
|
function readJsonl(file) {
|
|
6059
6162
|
const path = memPath(file);
|
|
6060
|
-
if (!
|
|
6163
|
+
if (!existsSync13(path)) return [];
|
|
6061
6164
|
const out = [];
|
|
6062
|
-
for (const line of
|
|
6165
|
+
for (const line of readFileSync13(path, "utf-8").split("\n")) {
|
|
6063
6166
|
const trimmed = line.trim();
|
|
6064
6167
|
if (!trimmed) continue;
|
|
6065
6168
|
try {
|
|
@@ -6071,13 +6174,13 @@ function readJsonl(file) {
|
|
|
6071
6174
|
}
|
|
6072
6175
|
function appendJsonl(file, obj) {
|
|
6073
6176
|
try {
|
|
6074
|
-
|
|
6177
|
+
appendFileSync4(memPath(file), JSON.stringify(obj) + "\n");
|
|
6075
6178
|
} catch {
|
|
6076
6179
|
}
|
|
6077
6180
|
}
|
|
6078
6181
|
function rewriteJsonl(file, rows) {
|
|
6079
6182
|
try {
|
|
6080
|
-
|
|
6183
|
+
writeFileSync11(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
|
|
6081
6184
|
} catch {
|
|
6082
6185
|
}
|
|
6083
6186
|
}
|
|
@@ -6086,7 +6189,7 @@ function scrubText(text) {
|
|
|
6086
6189
|
}
|
|
6087
6190
|
function addFact(input) {
|
|
6088
6191
|
const fact = {
|
|
6089
|
-
id:
|
|
6192
|
+
id: randomUUID5(),
|
|
6090
6193
|
text: scrubText(input.text),
|
|
6091
6194
|
kind: input.kind ?? "fact",
|
|
6092
6195
|
source: input.source ?? "user",
|
|
@@ -6113,7 +6216,7 @@ function summarizeAnswer(answer) {
|
|
|
6113
6216
|
}
|
|
6114
6217
|
function recordAnalysis(input) {
|
|
6115
6218
|
const entry = {
|
|
6116
|
-
id:
|
|
6219
|
+
id: randomUUID5(),
|
|
6117
6220
|
question: scrubText(input.question).slice(0, 300),
|
|
6118
6221
|
summary: scrubText(summarizeAnswer(input.answer)),
|
|
6119
6222
|
tools: input.tools,
|
|
@@ -6143,9 +6246,9 @@ function loadWinSnippets() {
|
|
|
6143
6246
|
try {
|
|
6144
6247
|
const dir = getWinsDir();
|
|
6145
6248
|
const out = [];
|
|
6146
|
-
for (const name of
|
|
6249
|
+
for (const name of readdirSync4(dir)) {
|
|
6147
6250
|
if (!name.endsWith(".md") || name.toLowerCase() === "readme.md") continue;
|
|
6148
|
-
const raw =
|
|
6251
|
+
const raw = readFileSync13(join14(dir, name), "utf-8");
|
|
6149
6252
|
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? name.replace(/\.md$/, "");
|
|
6150
6253
|
const body = raw.replace(/^#.*$/m, "").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
6151
6254
|
out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });
|
|
@@ -6347,7 +6450,7 @@ async function distillSessionFactsWithTimeout(ctx, sessionId, timeoutMs = DISTIL
|
|
|
6347
6450
|
});
|
|
6348
6451
|
const raced = await Promise.race([
|
|
6349
6452
|
work,
|
|
6350
|
-
new Promise((
|
|
6453
|
+
new Promise((resolve11) => setTimeout(() => resolve11(-1), timeoutMs))
|
|
6351
6454
|
]);
|
|
6352
6455
|
if (raced >= 0) return { count: raced, background };
|
|
6353
6456
|
if (settled) return { count: await background, background };
|
|
@@ -6425,10 +6528,10 @@ __export(context_exports, {
|
|
|
6425
6528
|
setPrimaryLens: () => setPrimaryLens,
|
|
6426
6529
|
transcriptPathForSession: () => transcriptPathForSession
|
|
6427
6530
|
});
|
|
6428
|
-
import { basename as
|
|
6429
|
-
import { existsSync as
|
|
6430
|
-
import { homedir as
|
|
6431
|
-
import { randomUUID as
|
|
6531
|
+
import { basename as basename3, join as join15, resolve as resolve7, sep as sep4 } from "path";
|
|
6532
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync12, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync3, rmSync as rmSync4 } from "fs";
|
|
6533
|
+
import { homedir as homedir4 } from "os";
|
|
6534
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
6432
6535
|
function isSessionStale(s) {
|
|
6433
6536
|
return Date.now() - s.mtime > STALE_SESSION_MS;
|
|
6434
6537
|
}
|
|
@@ -6441,35 +6544,35 @@ function isAnalysisReady(ctx) {
|
|
|
6441
6544
|
return Object.values(counts).some((n) => n > 0);
|
|
6442
6545
|
}
|
|
6443
6546
|
function ntrpHomeDir() {
|
|
6444
|
-
return process.env.NTRP_HOME ?
|
|
6547
|
+
return process.env.NTRP_HOME ? resolve7(process.env.NTRP_HOME) : join15(homedir4(), ".ntrp");
|
|
6445
6548
|
}
|
|
6446
6549
|
function getSessionsDir() {
|
|
6447
|
-
const dir =
|
|
6448
|
-
if (!
|
|
6449
|
-
|
|
6550
|
+
const dir = join15(ntrpHomeDir(), "sessions");
|
|
6551
|
+
if (!existsSync14(dir)) {
|
|
6552
|
+
mkdirSync6(dir, { recursive: true });
|
|
6450
6553
|
}
|
|
6451
6554
|
return dir;
|
|
6452
6555
|
}
|
|
6453
6556
|
function getDatasetsDir() {
|
|
6454
|
-
const dir =
|
|
6455
|
-
if (!
|
|
6456
|
-
|
|
6557
|
+
const dir = join15(ntrpHomeDir(), "datasets");
|
|
6558
|
+
if (!existsSync14(dir)) {
|
|
6559
|
+
mkdirSync6(dir, { recursive: true });
|
|
6457
6560
|
}
|
|
6458
6561
|
return dir;
|
|
6459
6562
|
}
|
|
6460
6563
|
function datasetPathForSession(id) {
|
|
6461
|
-
return
|
|
6564
|
+
return join15(getDatasetsDir(), `${id}.duckdb`);
|
|
6462
6565
|
}
|
|
6463
6566
|
function transcriptPathForSession(id) {
|
|
6464
|
-
return
|
|
6567
|
+
return join15(getSessionsDir(), `${id}.transcript.md`);
|
|
6465
6568
|
}
|
|
6466
6569
|
function contextDocPathForSession(id) {
|
|
6467
|
-
return
|
|
6570
|
+
return join15(getSessionsDir(), `${id}.context.md`);
|
|
6468
6571
|
}
|
|
6469
6572
|
function makeSessionId() {
|
|
6470
6573
|
const now2 = /* @__PURE__ */ new Date();
|
|
6471
6574
|
const date = now2.toISOString().slice(0, 10);
|
|
6472
|
-
const uuid2 =
|
|
6575
|
+
const uuid2 = randomUUID6().slice(0, 4);
|
|
6473
6576
|
return `${date}-${uuid2}`;
|
|
6474
6577
|
}
|
|
6475
6578
|
function isValidSessionId(id) {
|
|
@@ -6477,14 +6580,14 @@ function isValidSessionId(id) {
|
|
|
6477
6580
|
}
|
|
6478
6581
|
function sessionPathForId(id) {
|
|
6479
6582
|
if (!isValidSessionId(id)) return null;
|
|
6480
|
-
const dir =
|
|
6481
|
-
const filePath =
|
|
6482
|
-
if (filePath !== dir && !filePath.startsWith(dir +
|
|
6583
|
+
const dir = resolve7(getSessionsDir());
|
|
6584
|
+
const filePath = resolve7(dir, `${id}.json`);
|
|
6585
|
+
if (filePath !== dir && !filePath.startsWith(dir + sep4)) return null;
|
|
6483
6586
|
return filePath;
|
|
6484
6587
|
}
|
|
6485
6588
|
function initContext(oneShot, execution) {
|
|
6486
6589
|
const sessionId = makeSessionId();
|
|
6487
|
-
const sessionFile =
|
|
6590
|
+
const sessionFile = join15(getSessionsDir(), `${sessionId}.json`);
|
|
6488
6591
|
return {
|
|
6489
6592
|
sessionId,
|
|
6490
6593
|
sessionFile,
|
|
@@ -6598,7 +6701,7 @@ function recordMessage(ctx, role, content) {
|
|
|
6598
6701
|
ctx.messages.push(msg);
|
|
6599
6702
|
if (ctx.oneShot) return;
|
|
6600
6703
|
try {
|
|
6601
|
-
|
|
6704
|
+
writeFileSync12(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
|
|
6602
6705
|
} catch {
|
|
6603
6706
|
}
|
|
6604
6707
|
writeSessionContextDoc(ctx);
|
|
@@ -6606,7 +6709,7 @@ function recordMessage(ctx, role, content) {
|
|
|
6606
6709
|
function saveSessionState(ctx) {
|
|
6607
6710
|
if (ctx.oneShot) return;
|
|
6608
6711
|
try {
|
|
6609
|
-
|
|
6712
|
+
writeFileSync12(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
|
|
6610
6713
|
} catch {
|
|
6611
6714
|
}
|
|
6612
6715
|
writeSessionContextDoc(ctx);
|
|
@@ -6615,9 +6718,9 @@ function getLastActivityRelative() {
|
|
|
6615
6718
|
const dir = getSessionsDir();
|
|
6616
6719
|
let mostRecent = 0;
|
|
6617
6720
|
try {
|
|
6618
|
-
for (const name of
|
|
6721
|
+
for (const name of readdirSync5(dir)) {
|
|
6619
6722
|
if (!name.endsWith(".json")) continue;
|
|
6620
|
-
const m =
|
|
6723
|
+
const m = statSync3(join15(dir, name)).mtimeMs;
|
|
6621
6724
|
if (m > mostRecent) mostRecent = m;
|
|
6622
6725
|
}
|
|
6623
6726
|
} catch {
|
|
@@ -6643,7 +6746,7 @@ function loadSessionFile(id) {
|
|
|
6643
6746
|
const filePath = sessionPathForId(id);
|
|
6644
6747
|
if (!filePath) return null;
|
|
6645
6748
|
try {
|
|
6646
|
-
const raw =
|
|
6749
|
+
const raw = readFileSync14(filePath, "utf-8");
|
|
6647
6750
|
const session = JSON.parse(raw);
|
|
6648
6751
|
if (session.thread?.length) {
|
|
6649
6752
|
session.thread = normalizeThread(session.thread);
|
|
@@ -6657,16 +6760,16 @@ function listSessions(opts) {
|
|
|
6657
6760
|
const dir = getSessionsDir();
|
|
6658
6761
|
const entries = [];
|
|
6659
6762
|
try {
|
|
6660
|
-
const files =
|
|
6661
|
-
const filePath =
|
|
6662
|
-
return { name, filePath, mtime:
|
|
6763
|
+
const files = readdirSync5(dir).filter((name) => name.endsWith(".json")).map((name) => {
|
|
6764
|
+
const filePath = join15(dir, name);
|
|
6765
|
+
return { name, filePath, mtime: statSync3(filePath).mtimeMs };
|
|
6663
6766
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
6664
6767
|
const filesToRead = opts?.limit ? files.slice(0, opts.limit) : files;
|
|
6665
6768
|
for (const { name, filePath, mtime } of filesToRead) {
|
|
6666
|
-
const id =
|
|
6769
|
+
const id = basename3(name, ".json");
|
|
6667
6770
|
if (!isValidSessionId(id)) continue;
|
|
6668
6771
|
try {
|
|
6669
|
-
const raw =
|
|
6772
|
+
const raw = readFileSync14(filePath, "utf-8");
|
|
6670
6773
|
const session = JSON.parse(raw);
|
|
6671
6774
|
entries.push({
|
|
6672
6775
|
id,
|
|
@@ -6728,7 +6831,7 @@ async function closeAllActiveSessions(ctx) {
|
|
|
6728
6831
|
skipped.push(s.id);
|
|
6729
6832
|
continue;
|
|
6730
6833
|
}
|
|
6731
|
-
|
|
6834
|
+
writeFileSync12(filePath, JSON.stringify(file, null, 2) + "\n");
|
|
6732
6835
|
writeContextDocForSessionFile(file);
|
|
6733
6836
|
closed.push(s.id);
|
|
6734
6837
|
}
|
|
@@ -6768,7 +6871,7 @@ async function rotateToFreshSession(ctx) {
|
|
|
6768
6871
|
const newId = makeSessionId();
|
|
6769
6872
|
resetContextForSwitch(ctx, {
|
|
6770
6873
|
sessionId: newId,
|
|
6771
|
-
sessionFile:
|
|
6874
|
+
sessionFile: join15(getSessionsDir(), `${newId}.json`),
|
|
6772
6875
|
messages: [],
|
|
6773
6876
|
stage: "new",
|
|
6774
6877
|
analysis: defaultSessionAnalysis(),
|
|
@@ -6807,14 +6910,14 @@ async function finalizeSession(ctx, stage) {
|
|
|
6807
6910
|
}
|
|
6808
6911
|
for (const path of [ctx.datasetPath, `${ctx.datasetPath}.wal`]) {
|
|
6809
6912
|
try {
|
|
6810
|
-
|
|
6913
|
+
rmSync4(path, { force: true });
|
|
6811
6914
|
} catch {
|
|
6812
6915
|
}
|
|
6813
6916
|
}
|
|
6814
6917
|
}
|
|
6815
6918
|
discardSessionTranscript(ctx.sessionId);
|
|
6816
6919
|
try {
|
|
6817
|
-
|
|
6920
|
+
rmSync4(contextDocPathForSession(ctx.sessionId), { force: true });
|
|
6818
6921
|
} catch {
|
|
6819
6922
|
}
|
|
6820
6923
|
return void 0;
|
|
@@ -6869,7 +6972,7 @@ async function finalizeSession(ctx, stage) {
|
|
|
6869
6972
|
file.pending_ask = ctx.pendingAsk;
|
|
6870
6973
|
}
|
|
6871
6974
|
try {
|
|
6872
|
-
|
|
6975
|
+
writeFileSync12(ctx.sessionFile, JSON.stringify(file, null, 2) + "\n");
|
|
6873
6976
|
} catch {
|
|
6874
6977
|
}
|
|
6875
6978
|
writeContextDocForSessionFile(file, { snapshot: ctx.snapshot.computeResult });
|
|
@@ -7328,9 +7431,9 @@ var init_tool_schemas = __esm({
|
|
|
7328
7431
|
});
|
|
7329
7432
|
|
|
7330
7433
|
// src/ai/privacy.ts
|
|
7331
|
-
import { existsSync as
|
|
7332
|
-
import { homedir as
|
|
7333
|
-
import { join as
|
|
7434
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, appendFileSync as appendFileSync5 } from "fs";
|
|
7435
|
+
import { homedir as homedir5 } from "os";
|
|
7436
|
+
import { join as join16 } from "path";
|
|
7334
7437
|
function stripPII(obj) {
|
|
7335
7438
|
if (obj === null || obj === void 0) return obj;
|
|
7336
7439
|
if (typeof obj !== "object") return obj;
|
|
@@ -7345,15 +7448,15 @@ function stripPII(obj) {
|
|
|
7345
7448
|
return out;
|
|
7346
7449
|
}
|
|
7347
7450
|
function ensureAuditDir() {
|
|
7348
|
-
if (!
|
|
7349
|
-
|
|
7451
|
+
if (!existsSync15(AUDIT_DIR)) {
|
|
7452
|
+
mkdirSync7(AUDIT_DIR, { recursive: true });
|
|
7350
7453
|
}
|
|
7351
7454
|
}
|
|
7352
7455
|
function logToolCall(entry) {
|
|
7353
7456
|
ensureAuditDir();
|
|
7354
7457
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7355
|
-
const path =
|
|
7356
|
-
|
|
7458
|
+
const path = join16(AUDIT_DIR, `agentic-${date}.jsonl`);
|
|
7459
|
+
appendFileSync5(path, JSON.stringify(entry) + "\n");
|
|
7357
7460
|
}
|
|
7358
7461
|
var PII_FIELDS, AUDIT_DIR;
|
|
7359
7462
|
var init_privacy = __esm({
|
|
@@ -7376,7 +7479,7 @@ var init_privacy = __esm({
|
|
|
7376
7479
|
"raw_data",
|
|
7377
7480
|
"metadata"
|
|
7378
7481
|
]);
|
|
7379
|
-
AUDIT_DIR =
|
|
7482
|
+
AUDIT_DIR = join16(homedir5(), ".ntrp", "audit");
|
|
7380
7483
|
}
|
|
7381
7484
|
});
|
|
7382
7485
|
|
|
@@ -7434,17 +7537,17 @@ __export(play_outcomes_exports, {
|
|
|
7434
7537
|
listPlayOutcomes: () => listPlayOutcomes,
|
|
7435
7538
|
recordPlayOutcomes: () => recordPlayOutcomes
|
|
7436
7539
|
});
|
|
7437
|
-
import { existsSync as
|
|
7438
|
-
import { join as
|
|
7439
|
-
import { randomUUID as
|
|
7540
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15, appendFileSync as appendFileSync6 } from "fs";
|
|
7541
|
+
import { join as join17 } from "path";
|
|
7542
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
7440
7543
|
function outcomesPath() {
|
|
7441
|
-
return
|
|
7544
|
+
return join17(getMemoryDir(), OUTCOMES_FILE);
|
|
7442
7545
|
}
|
|
7443
7546
|
function listPlayOutcomes() {
|
|
7444
7547
|
const path = outcomesPath();
|
|
7445
|
-
if (!
|
|
7548
|
+
if (!existsSync16(path)) return [];
|
|
7446
7549
|
const out = [];
|
|
7447
|
-
for (const line of
|
|
7550
|
+
for (const line of readFileSync15(path, "utf-8").split("\n")) {
|
|
7448
7551
|
const trimmed = line.trim();
|
|
7449
7552
|
if (!trimmed) continue;
|
|
7450
7553
|
try {
|
|
@@ -7474,7 +7577,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
|
|
|
7474
7577
|
if (seen.has(key)) continue;
|
|
7475
7578
|
seen.add(key);
|
|
7476
7579
|
const record = {
|
|
7477
|
-
id:
|
|
7580
|
+
id: randomUUID7(),
|
|
7478
7581
|
play_id: playId,
|
|
7479
7582
|
strategy_slug: strategy.slug,
|
|
7480
7583
|
workstream_order: outcome.workstream_order,
|
|
@@ -7487,7 +7590,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
|
|
|
7487
7590
|
reviewed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7488
7591
|
};
|
|
7489
7592
|
try {
|
|
7490
|
-
|
|
7593
|
+
appendFileSync6(outcomesPath(), JSON.stringify(record) + "\n");
|
|
7491
7594
|
written++;
|
|
7492
7595
|
} catch {
|
|
7493
7596
|
}
|
|
@@ -7748,16 +7851,16 @@ var init_metrics_benchmarks = __esm({
|
|
|
7748
7851
|
});
|
|
7749
7852
|
|
|
7750
7853
|
// src/config/profile.ts
|
|
7751
|
-
import { readFileSync as
|
|
7752
|
-
import { join as
|
|
7854
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, existsSync as existsSync17, mkdirSync as mkdirSync8 } from "fs";
|
|
7855
|
+
import { join as join18 } from "path";
|
|
7753
7856
|
function isProfileConfigured(profile = loadProfile()) {
|
|
7754
7857
|
if (!profile) return false;
|
|
7755
7858
|
return profile.company_name.trim().length > 0;
|
|
7756
7859
|
}
|
|
7757
7860
|
function loadProfile() {
|
|
7758
|
-
if (!
|
|
7861
|
+
if (!existsSync17(PROFILE_PATH)) return null;
|
|
7759
7862
|
try {
|
|
7760
|
-
const parsed = JSON.parse(
|
|
7863
|
+
const parsed = JSON.parse(readFileSync16(PROFILE_PATH, "utf-8"));
|
|
7761
7864
|
if (!parsed || typeof parsed !== "object") return null;
|
|
7762
7865
|
return parsed;
|
|
7763
7866
|
} catch {
|
|
@@ -7770,7 +7873,7 @@ var init_profile = __esm({
|
|
|
7770
7873
|
"use strict";
|
|
7771
7874
|
init_store();
|
|
7772
7875
|
NTRP_DIR3 = ntrpHome();
|
|
7773
|
-
PROFILE_PATH =
|
|
7876
|
+
PROFILE_PATH = join18(NTRP_DIR3, "profile.json");
|
|
7774
7877
|
}
|
|
7775
7878
|
});
|
|
7776
7879
|
|
|
@@ -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((
|
|
10628
|
+
return await new Promise((resolve11, 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
|
-
|
|
10664
|
+
resolve11(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
|
-
|
|
10680
|
+
resolve11(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
|
|
11544
|
-
import { join as
|
|
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 =
|
|
11716
|
+
const path = join19(ntrpHome(), ANALYST_FILE_NAME);
|
|
11564
11717
|
try {
|
|
11565
|
-
if (!
|
|
11566
|
-
const raw =
|
|
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/
|
|
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 ?
|
|
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))} ${
|
|
12318
|
-
console.log(` ${dot} ${label} ${bar} ${
|
|
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 = `${
|
|
12322
|
-
const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${paint("success", formatCurrency(result.total_value_at_risk))} ${
|
|
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
|
-
`${
|
|
12326
|
-
`${
|
|
12327
|
-
`${
|
|
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 = `${
|
|
12506
|
+
const score = `${chalk10.bold(String(Math.round(result.overall_score)))}${chalk10.dim("/100")}`;
|
|
12333
12507
|
const parts = [
|
|
12334
|
-
`${
|
|
12335
|
-
`${
|
|
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))} ${
|
|
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(
|
|
12342
|
-
|
|
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 =
|
|
12362
|
-
console.log(` ${dot} ${name} ${
|
|
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(" " +
|
|
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))} ${
|
|
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
|
-
" " +
|
|
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")}` +
|
|
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(` ${
|
|
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 =
|
|
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(` ${
|
|
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 ? ` ${
|
|
12443
|
-
console.log(` ${dot} ${
|
|
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
|
-
` ${
|
|
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
|
-
|
|
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(
|
|
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: [
|
|
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(
|
|
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))} ${
|
|
12490
|
-
console.log(` ${dot} ${label} ${bar} ${
|
|
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(
|
|
12785
|
+
console.log(chalk10.dim(" No findings generated."));
|
|
12602
12786
|
console.log();
|
|
12603
12787
|
}
|
|
12604
12788
|
if (toolCalls > 0) {
|
|
12605
12789
|
console.log(
|
|
12606
|
-
|
|
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(
|
|
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 ?
|
|
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 ?
|
|
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(
|
|
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
|
|
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(
|
|
12681
|
-
console.log(" " +
|
|
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(" " +
|
|
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} ${
|
|
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(` ${
|
|
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 ?
|
|
12740
|
-
const expStr = b.expansion_arr > 0 ?
|
|
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
|
-
` ${
|
|
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",
|
|
@@ -13327,7 +13532,7 @@ var diagnose_exports = {};
|
|
|
13327
13532
|
__export(diagnose_exports, {
|
|
13328
13533
|
handler: () => handler
|
|
13329
13534
|
});
|
|
13330
|
-
import
|
|
13535
|
+
import chalk12 from "chalk";
|
|
13331
13536
|
async function handler(args, ctx) {
|
|
13332
13537
|
await hydrateAnalysisFromPersistedState(ctx);
|
|
13333
13538
|
const { flags } = parseArgs(args, ["findings", "deep", "compact"]);
|
|
@@ -13356,9 +13561,9 @@ async function handler(args, ctx) {
|
|
|
13356
13561
|
}
|
|
13357
13562
|
if (options.findings && !canUseReplAi(ctx)) {
|
|
13358
13563
|
console.log();
|
|
13359
|
-
console.log(" " +
|
|
13360
|
-
console.log(" " +
|
|
13361
|
-
console.log(" " +
|
|
13564
|
+
console.log(" " + chalk12.red("AI findings run only in the interactive REPL."));
|
|
13565
|
+
console.log(" " + chalk12.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
|
|
13566
|
+
console.log(" " + chalk12.dim("Start with ") + paint("accent", "ntrp") + chalk12.dim(", run ") + paint("accent", "/connect") + chalk12.dim(" (any provider key), then /diagnose --findings."));
|
|
13362
13567
|
console.log();
|
|
13363
13568
|
return;
|
|
13364
13569
|
}
|
|
@@ -13390,7 +13595,7 @@ async function handler(args, ctx) {
|
|
|
13390
13595
|
}
|
|
13391
13596
|
ctx.skipTimeBankDiagnoseCredit = false;
|
|
13392
13597
|
if (ctx.oneShot && options.findings) {
|
|
13393
|
-
console.log(
|
|
13598
|
+
console.log(chalk12.dim(" For follow-up questions, run `ntrp` and ask in plain English."));
|
|
13394
13599
|
console.log();
|
|
13395
13600
|
}
|
|
13396
13601
|
return summary;
|
|
@@ -13448,7 +13653,7 @@ async function runDiagnose(options, ctx) {
|
|
|
13448
13653
|
});
|
|
13449
13654
|
return buildDiagnoseSummary(fullResult.aggregate, findings);
|
|
13450
13655
|
} catch (err) {
|
|
13451
|
-
console.error(
|
|
13656
|
+
console.error(chalk12.red(String(err)));
|
|
13452
13657
|
process.exit(1);
|
|
13453
13658
|
}
|
|
13454
13659
|
}
|
|
@@ -13460,7 +13665,7 @@ async function runSegmentDiagnose(options) {
|
|
|
13460
13665
|
spinner.succeed("Diagnosis complete");
|
|
13461
13666
|
} catch (err) {
|
|
13462
13667
|
spinner.fail("Diagnosis failed");
|
|
13463
|
-
console.error(
|
|
13668
|
+
console.error(chalk12.red(String(err)));
|
|
13464
13669
|
process.exit(1);
|
|
13465
13670
|
}
|
|
13466
13671
|
const needle = options.segment.toLowerCase();
|
|
@@ -13469,19 +13674,19 @@ async function runSegmentDiagnose(options) {
|
|
|
13469
13674
|
const subs = result.segments.filter((s) => s.segment.name.toLowerCase().includes(needle));
|
|
13470
13675
|
if (subs.length === 1) match = subs[0];
|
|
13471
13676
|
else if (subs.length > 1) {
|
|
13472
|
-
console.error(
|
|
13677
|
+
console.error(chalk12.yellow(`
|
|
13473
13678
|
"${options.segment}" matches multiple segments:`));
|
|
13474
|
-
for (const s of subs) console.log(
|
|
13679
|
+
for (const s of subs) console.log(chalk12.dim(` - ${s.segment.name}`));
|
|
13475
13680
|
console.log();
|
|
13476
13681
|
return;
|
|
13477
13682
|
}
|
|
13478
13683
|
}
|
|
13479
13684
|
if (!match) {
|
|
13480
|
-
console.error(
|
|
13685
|
+
console.error(chalk12.red(`
|
|
13481
13686
|
No segment matching "${options.segment}".`));
|
|
13482
13687
|
if (result.segments.length > 0) {
|
|
13483
|
-
console.log(
|
|
13484
|
-
for (const s of result.segments) console.log(
|
|
13688
|
+
console.log(chalk12.dim(" Available segments:"));
|
|
13689
|
+
for (const s of result.segments) console.log(chalk12.dim(` - ${s.segment.name}`));
|
|
13485
13690
|
}
|
|
13486
13691
|
console.log();
|
|
13487
13692
|
return;
|
|
@@ -13535,145 +13740,1089 @@ var init_diagnose = __esm({
|
|
|
13535
13740
|
}
|
|
13536
13741
|
});
|
|
13537
13742
|
|
|
13538
|
-
// src/services/session-analysis.ts
|
|
13539
|
-
async function loadSessionAnalysisBundle() {
|
|
13540
|
-
const [diagnosis, metrics] = await Promise.all([
|
|
13541
|
-
loadLatestDiagnosis(),
|
|
13542
|
-
loadLatestMetricsAnalysis()
|
|
13543
|
-
]);
|
|
13544
|
-
return { diagnosis, metrics };
|
|
13743
|
+
// src/services/session-analysis.ts
|
|
13744
|
+
async function loadSessionAnalysisBundle() {
|
|
13745
|
+
const [diagnosis, metrics] = await Promise.all([
|
|
13746
|
+
loadLatestDiagnosis(),
|
|
13747
|
+
loadLatestMetricsAnalysis()
|
|
13748
|
+
]);
|
|
13749
|
+
return { diagnosis, metrics };
|
|
13750
|
+
}
|
|
13751
|
+
function hasAnyAnalysis(bundle) {
|
|
13752
|
+
return bundle.diagnosis != null || bundle.metrics != null;
|
|
13753
|
+
}
|
|
13754
|
+
function formatMetricLine(row) {
|
|
13755
|
+
const label = row.label ?? row.metric;
|
|
13756
|
+
const formatted = row.formatted ?? "--";
|
|
13757
|
+
const conf = row.confidence;
|
|
13758
|
+
const confStr = conf != null && conf < 80 ? ` (${conf}% conf)` : "";
|
|
13759
|
+
return `- ${label}: ${formatted}${confStr}`;
|
|
13760
|
+
}
|
|
13761
|
+
function buildHandoffContextBlock(bundle, ctx) {
|
|
13762
|
+
const { diagnosis, metrics } = bundle;
|
|
13763
|
+
const profile = loadProfile();
|
|
13764
|
+
const lines = [];
|
|
13765
|
+
if (profile?.company_name) {
|
|
13766
|
+
lines.push(`Company: ${profile.company_name} (${profile.industry})`);
|
|
13767
|
+
lines.push(`Sales motion: ${profile.sales_motion}${profile.average_deal_size ? ` \xB7 avg deal ${profile.average_deal_size}` : ""}`);
|
|
13768
|
+
if (profile.user_scope) lines.push(`My scope: ${profile.user_scope}`);
|
|
13769
|
+
}
|
|
13770
|
+
if (ctx.dataset?.label) {
|
|
13771
|
+
const counts = ctx.dataset.counts ?? {};
|
|
13772
|
+
const countStr = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k}`).join(", ");
|
|
13773
|
+
lines.push(`Dataset: ${ctx.dataset.label}${countStr ? ` (${countStr})` : ""}`);
|
|
13774
|
+
}
|
|
13775
|
+
const completed = ctx.analysis.completed;
|
|
13776
|
+
if (completed.length > 0) {
|
|
13777
|
+
lines.push(`Analysis lenses completed: ${completed.join(", ")}`);
|
|
13778
|
+
}
|
|
13779
|
+
lines.push("");
|
|
13780
|
+
if (diagnosis) {
|
|
13781
|
+
const { health, findings } = diagnosis;
|
|
13782
|
+
lines.push("## GTM health (vital signs)");
|
|
13783
|
+
lines.push(`Overall score: ${Math.round(health.overall_score)} (${health.overall_status})`);
|
|
13784
|
+
if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
|
|
13785
|
+
lines.push(`Total value at risk: ${formatCurrency(health.total_value_at_risk)}`);
|
|
13786
|
+
}
|
|
13787
|
+
lines.push("");
|
|
13788
|
+
lines.push("### Vital signs");
|
|
13789
|
+
for (const vs of health.vital_signs) {
|
|
13790
|
+
const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
|
|
13791
|
+
const dollars = vs.dollar_value != null ? ` \u2014 ${formatCurrency(vs.dollar_value)}${vs.dollar_label ? ` ${vs.dollar_label}` : ""}` : "";
|
|
13792
|
+
lines.push(`- ${label}: ${Math.round(vs.score)} (${vs.status})${dollars}`);
|
|
13793
|
+
}
|
|
13794
|
+
if (findings.length > 0) {
|
|
13795
|
+
lines.push("");
|
|
13796
|
+
lines.push("### GTM findings");
|
|
13797
|
+
appendFindings(lines, findings);
|
|
13798
|
+
}
|
|
13799
|
+
lines.push("");
|
|
13800
|
+
}
|
|
13801
|
+
if (metrics && metrics.metrics.length > 0) {
|
|
13802
|
+
lines.push("## SaaS metrics");
|
|
13803
|
+
const byKey = new Map(metrics.metrics.map((r) => [r.metric, r]));
|
|
13804
|
+
for (const key of KEY_METRICS) {
|
|
13805
|
+
const row = byKey.get(key);
|
|
13806
|
+
if (row) lines.push(formatMetricLine(row));
|
|
13807
|
+
}
|
|
13808
|
+
if (metrics.findings.length > 0) {
|
|
13809
|
+
lines.push("");
|
|
13810
|
+
lines.push("### Metrics findings");
|
|
13811
|
+
appendFindings(lines, metrics.findings);
|
|
13812
|
+
}
|
|
13813
|
+
lines.push("");
|
|
13814
|
+
}
|
|
13815
|
+
if (!diagnosis && metrics) {
|
|
13816
|
+
lines.unshift("Primary analysis: SaaS metrics (no GTM health snapshot on this session yet).", "");
|
|
13817
|
+
} else if (diagnosis && !metrics) {
|
|
13818
|
+
lines.push("(SaaS metrics not run on this session \u2014 run /metrics for the revenue view)");
|
|
13819
|
+
}
|
|
13820
|
+
return lines.join("\n").trim();
|
|
13821
|
+
}
|
|
13822
|
+
function buildExploreContextBlock(bundle, ctx) {
|
|
13823
|
+
const full = buildHandoffContextBlock(bundle, ctx);
|
|
13824
|
+
if (!full) return "";
|
|
13825
|
+
const lines = full.split("\n");
|
|
13826
|
+
const out = [
|
|
13827
|
+
"COMPLETED ANALYSIS (the user already saw the full report \u2014 cite this, do not re-dump it):",
|
|
13828
|
+
""
|
|
13829
|
+
];
|
|
13830
|
+
let inFindings = false;
|
|
13831
|
+
let findingCount = 0;
|
|
13832
|
+
for (const line of lines) {
|
|
13833
|
+
if (line.startsWith("### GTM findings") || line.startsWith("### Metrics findings")) {
|
|
13834
|
+
inFindings = true;
|
|
13835
|
+
out.push(line);
|
|
13836
|
+
continue;
|
|
13837
|
+
}
|
|
13838
|
+
if (inFindings && line.startsWith("- [")) {
|
|
13839
|
+
if (findingCount >= 5) continue;
|
|
13840
|
+
out.push(line);
|
|
13841
|
+
findingCount++;
|
|
13842
|
+
continue;
|
|
13843
|
+
}
|
|
13844
|
+
if (inFindings && line.startsWith("##")) {
|
|
13845
|
+
inFindings = false;
|
|
13846
|
+
}
|
|
13847
|
+
if (line.startsWith("## ") || line.startsWith("### Vital") || line.startsWith("- ") && !inFindings) {
|
|
13848
|
+
if (line.startsWith("(SaaS metrics not run")) continue;
|
|
13849
|
+
out.push(line);
|
|
13850
|
+
}
|
|
13851
|
+
if (line.startsWith("Overall score:") || line.startsWith("Total value at risk:")) {
|
|
13852
|
+
out.push(line);
|
|
13853
|
+
}
|
|
13854
|
+
if (line.startsWith("- ARR:") || line.startsWith("- NRR:") || line.startsWith("- GRR:")) {
|
|
13855
|
+
out.push(line);
|
|
13856
|
+
}
|
|
13857
|
+
}
|
|
13858
|
+
return out.join("\n").trim();
|
|
13859
|
+
}
|
|
13860
|
+
function appendFindings(lines, findings) {
|
|
13861
|
+
for (const f of findings.slice(0, 12)) {
|
|
13862
|
+
const dollars = f.dollar_value != null ? ` (${formatCurrency(f.dollar_value)})` : "";
|
|
13863
|
+
const plays = f.recommended_plays?.length ? ` \u2192 Plays: ${f.recommended_plays.map((p) => p.play_name).join(", ")}` : "";
|
|
13864
|
+
lines.push(`- [${f.severity}]${dollars} ${f.finding}${plays}`);
|
|
13865
|
+
}
|
|
13866
|
+
}
|
|
13867
|
+
function handoffInstructionPrefix(primary) {
|
|
13868
|
+
if (primary === "revenue_metrics") {
|
|
13869
|
+
return "the SaaS metrics and pipeline context below";
|
|
13870
|
+
}
|
|
13871
|
+
return "the pipeline diagnosis below";
|
|
13872
|
+
}
|
|
13873
|
+
var KEY_METRICS;
|
|
13874
|
+
var init_session_analysis = __esm({
|
|
13875
|
+
"src/services/session-analysis.ts"() {
|
|
13876
|
+
"use strict";
|
|
13877
|
+
init_queries();
|
|
13878
|
+
init_profile();
|
|
13879
|
+
init_formatters();
|
|
13880
|
+
init_theme();
|
|
13881
|
+
KEY_METRICS = ["arr", "nrr", "grr", "win_rate", "pipeline_coverage"];
|
|
13882
|
+
}
|
|
13883
|
+
});
|
|
13884
|
+
|
|
13885
|
+
// src/data/metric-definitions.ts
|
|
13886
|
+
function pctBand(metric, motion) {
|
|
13887
|
+
const m = motion ?? "mid_market";
|
|
13888
|
+
const t = METRICS_BENCHMARKS[m][metric];
|
|
13889
|
+
return `${motionBenchmarkLabel(m)} green \u2265${t.green}${metric === "pipeline_coverage" ? "x" : "%"}, yellow \u2265${t.yellow}${metric === "pipeline_coverage" ? "x" : "%"}`;
|
|
13890
|
+
}
|
|
13891
|
+
function monthsBand(motion) {
|
|
13892
|
+
const m = motion ?? "mid_market";
|
|
13893
|
+
const t = METRICS_BENCHMARKS[m].payback_months;
|
|
13894
|
+
return `${motionBenchmarkLabel(m)} green \u2264${t.green}mo, yellow \u2264${t.yellow}mo`;
|
|
13895
|
+
}
|
|
13896
|
+
function magicBand(motion) {
|
|
13897
|
+
const m = motion ?? "mid_market";
|
|
13898
|
+
const t = METRICS_BENCHMARKS[m].magic_number;
|
|
13899
|
+
return `${motionBenchmarkLabel(m)} green \u2265${t.green}, yellow \u2265${t.yellow}`;
|
|
13900
|
+
}
|
|
13901
|
+
function getMetricExplainer(id) {
|
|
13902
|
+
return BY_ID.get(id);
|
|
13903
|
+
}
|
|
13904
|
+
function resolveMetricId(query) {
|
|
13905
|
+
const q = query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
13906
|
+
if (!q) return void 0;
|
|
13907
|
+
if (BY_ID.has(q)) return q;
|
|
13908
|
+
const direct = ALIAS_INDEX.get(q);
|
|
13909
|
+
if (direct) return direct;
|
|
13910
|
+
const norm = q.replace(/[-\s]+/g, "_");
|
|
13911
|
+
if (BY_ID.has(norm)) return norm;
|
|
13912
|
+
return ALIAS_INDEX.get(norm);
|
|
13913
|
+
}
|
|
13914
|
+
var VITALS, SAAS, METRIC_DEFINITIONS, BY_ID, ALIAS_INDEX, SAAS_METRIC_IDS;
|
|
13915
|
+
var init_metric_definitions = __esm({
|
|
13916
|
+
"src/data/metric-definitions.ts"() {
|
|
13917
|
+
"use strict";
|
|
13918
|
+
init_metrics_benchmarks();
|
|
13919
|
+
VITALS = [
|
|
13920
|
+
{
|
|
13921
|
+
id: "freshness",
|
|
13922
|
+
kind: "vital",
|
|
13923
|
+
label: "Freshness",
|
|
13924
|
+
group: "Vital Signs",
|
|
13925
|
+
tagline: "Is your CRM telling the truth about what's alive?",
|
|
13926
|
+
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.",
|
|
13927
|
+
formula_lines: [
|
|
13928
|
+
"freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
|
|
13929
|
+
"people/orgs fresh if activity within 90d",
|
|
13930
|
+
"opps fresh if activity within 30d AND not past-due"
|
|
13931
|
+
],
|
|
13932
|
+
meaning: 'Board question: "how much of this pipeline is real vs fiction?" Dollar value = sum of amount on stale opportunities \u2014 pipeline at risk.',
|
|
13933
|
+
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.",
|
|
13934
|
+
deepdive: [
|
|
13935
|
+
"Status: green \u226580, yellow \u226560, red below 60 (motion presets can shift windows).",
|
|
13936
|
+
'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
|
|
13937
|
+
"Layer 1 of the gating stack \u2014 a red here bounds what you can trust downstream.",
|
|
13938
|
+
"Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when score < 60.",
|
|
13939
|
+
"Levers: stale-deal alert at N quiet days, weekly hygiene scrub, enrichment refresh on quiet records, signal-triggered reactivation for paid-for dormant accounts."
|
|
13940
|
+
],
|
|
13941
|
+
visual: {
|
|
13942
|
+
kind: "bars",
|
|
13943
|
+
caption: "Exemplar component mix (higher = fresher)",
|
|
13944
|
+
bars: [
|
|
13945
|
+
{ label: "People", value: 72, tone: "yellow" },
|
|
13946
|
+
{ label: "Organizations", value: 81, tone: "green" },
|
|
13947
|
+
{ label: "Opportunities", value: 44, tone: "red" }
|
|
13948
|
+
]
|
|
13949
|
+
},
|
|
13950
|
+
play_id: "clean-dead-pipeline",
|
|
13951
|
+
dollar_label: "pipeline at risk",
|
|
13952
|
+
audience: {
|
|
13953
|
+
board: "Freshness answers whether the pipeline number is real. Low freshness means forecast risk \u2014 stale deals inflate coverage and hide the true gap.",
|
|
13954
|
+
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."
|
|
13955
|
+
},
|
|
13956
|
+
aliases: ["data freshness", "stale", "zombie deals", "crm freshness"]
|
|
13957
|
+
},
|
|
13958
|
+
{
|
|
13959
|
+
id: "flow_rate",
|
|
13960
|
+
kind: "vital",
|
|
13961
|
+
label: "Flow Rate",
|
|
13962
|
+
group: "Vital Signs",
|
|
13963
|
+
tagline: "How fast do deals actually move \u2014 and where do they die?",
|
|
13964
|
+
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.",
|
|
13965
|
+
formula_lines: [
|
|
13966
|
+
"base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
|
|
13967
|
+
"score = base \u2212 stuckSharePenalty (\u226420)",
|
|
13968
|
+
"stuck = no update > stuck_days OR past-due close"
|
|
13969
|
+
],
|
|
13970
|
+
meaning: 'Board question: "is next quarter slipping because deals are stuck?" Dollar value = amount stuck in pipeline.',
|
|
13971
|
+
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.",
|
|
13972
|
+
deepdive: [
|
|
13973
|
+
"Status from avg open age: \u226445d green, \u226490d yellow, else red (defaults; max_days 120, stuck_days 60).",
|
|
13974
|
+
'Dollar translation: sum of amount on stuck deals \u2192 "stuck in pipeline".',
|
|
13975
|
+
"Layer 2 of the gating stack (with Drop Rate).",
|
|
13976
|
+
"Trigger play: Unstick the Pipeline (unstick-pipeline) when score is weak.",
|
|
13977
|
+
"Levers: stage-age report, past-due close cleanup, progression plans on stuck deals, forecast hygiene on happy-ears dates."
|
|
13978
|
+
],
|
|
13979
|
+
visual: {
|
|
13980
|
+
kind: "funnel",
|
|
13981
|
+
caption: "Exemplar stage ages \u2014 find the stage where deals go to die",
|
|
13982
|
+
funnel: [
|
|
13983
|
+
{ label: "Discovery", widthPct: 100 },
|
|
13984
|
+
{ label: "Qualify", widthPct: 78 },
|
|
13985
|
+
{ label: "Propose", widthPct: 55 },
|
|
13986
|
+
{ label: "Negotiate", widthPct: 22 },
|
|
13987
|
+
{ label: "Closed", widthPct: 12 }
|
|
13988
|
+
]
|
|
13989
|
+
},
|
|
13990
|
+
play_id: "unstick-pipeline",
|
|
13991
|
+
dollar_label: "stuck in pipeline",
|
|
13992
|
+
audience: {
|
|
13993
|
+
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.",
|
|
13994
|
+
ops: "Find the stage with collapsing advancement and age. Clear past-due closes, write progression plans on stuck deals. Play: Unstick the Pipeline."
|
|
13995
|
+
},
|
|
13996
|
+
aliases: ["flow rate", "deal velocity", "stuck deals", "stuck pipeline"]
|
|
13997
|
+
},
|
|
13998
|
+
{
|
|
13999
|
+
id: "drop_rate",
|
|
14000
|
+
kind: "vital",
|
|
14001
|
+
label: "Drop Rate",
|
|
14002
|
+
group: "Vital Signs",
|
|
14003
|
+
tagline: "Where do leads vanish between systems?",
|
|
14004
|
+
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.",
|
|
14005
|
+
formula_lines: [
|
|
14006
|
+
"score = crossSystemRetention\xD70.6 + oppRetention\xD70.4",
|
|
14007
|
+
"cross-system = marketing people also in sales CRM",
|
|
14008
|
+
"abandoned = open opps with no activity in 30d"
|
|
14009
|
+
],
|
|
14010
|
+
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.',
|
|
14011
|
+
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.",
|
|
14012
|
+
deepdive: [
|
|
14013
|
+
"Status: green \u226580, yellow \u226560, red below 60.",
|
|
14014
|
+
'Dollar translation: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
|
|
14015
|
+
"Layer 2 of the gating stack (with Flow Rate).",
|
|
14016
|
+
"Trigger play: Fix the Handoff Gap (fix-handoff-gap) when drop is high.",
|
|
14017
|
+
"Levers: source-level handoff audit, routing + sync repair, time-to-first-touch SLA, weekly marketing-only-leads report."
|
|
14018
|
+
],
|
|
14019
|
+
visual: {
|
|
14020
|
+
kind: "funnel",
|
|
14021
|
+
caption: "Exemplar handoff funnel \u2014 the leak is usually one or two sources",
|
|
14022
|
+
funnel: [
|
|
14023
|
+
{ label: "Marketing leads", widthPct: 100 },
|
|
14024
|
+
{ label: "In sales CRM", widthPct: 62 },
|
|
14025
|
+
{ label: "Assigned + touched", widthPct: 41 },
|
|
14026
|
+
{ label: "Active opportunities", widthPct: 28 }
|
|
14027
|
+
]
|
|
14028
|
+
},
|
|
14029
|
+
play_id: "fix-handoff-gap",
|
|
14030
|
+
dollar_label: "est. lost at handoff",
|
|
14031
|
+
audience: {
|
|
14032
|
+
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.",
|
|
14033
|
+
ops: "Audit by source, fix routing/sync/dead queues, instrument time-to-first-touch. Play: Fix the Handoff Gap."
|
|
14034
|
+
},
|
|
14035
|
+
aliases: ["drop rate", "handoff", "handoff gap", "lead leak", "marketing sales handoff"]
|
|
14036
|
+
},
|
|
14037
|
+
{
|
|
14038
|
+
id: "signal_to_noise",
|
|
14039
|
+
kind: "vital",
|
|
14040
|
+
label: "Signal:Noise",
|
|
14041
|
+
group: "Vital Signs",
|
|
14042
|
+
tagline: "How much activity is aimed at deals that can still close?",
|
|
14043
|
+
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.",
|
|
14044
|
+
formula_lines: [
|
|
14045
|
+
"score = (signalCount / activityCount) \xD7 100",
|
|
14046
|
+
"signal = linked to open opp / pipeline person / pipeline org",
|
|
14047
|
+
"lookback = trailing 90 days"
|
|
14048
|
+
],
|
|
14049
|
+
meaning: 'Board question: "are we burning capacity on dead water?" Dollar value = noiseCount \xD7 hours_per_activity \xD7 rep_hourly_cost \u2014 misdirected effort.',
|
|
14050
|
+
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.",
|
|
14051
|
+
deepdive: [
|
|
14052
|
+
"Status: green \u226565, yellow \u226540, red below 40.",
|
|
14053
|
+
"Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost).",
|
|
14054
|
+
"Layer 3 of the gating stack \u2014 trust Freshness / Flow / Drop before reading activity efficiency.",
|
|
14055
|
+
"Trigger play: Retarget Misdirected Effort (retarget-effort) when score is low.",
|
|
14056
|
+
"Levers: refresh account lists, signal-based targeting, stop logging against closed/unlinked records, coverage-model redesign."
|
|
14057
|
+
],
|
|
14058
|
+
visual: {
|
|
14059
|
+
kind: "split",
|
|
14060
|
+
caption: "Exemplar activity mix \u2014 signal vs noise",
|
|
14061
|
+
bars: [
|
|
14062
|
+
{ label: "Signal", value: 38, tone: "green" },
|
|
14063
|
+
{ label: "Noise", value: 62, tone: "red" }
|
|
14064
|
+
]
|
|
14065
|
+
},
|
|
14066
|
+
play_id: "retarget-effort",
|
|
14067
|
+
dollar_label: "misdirected effort",
|
|
14068
|
+
audience: {
|
|
14069
|
+
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.",
|
|
14070
|
+
ops: "Score = % of activities linked to live pipeline. Cut by rep and account status; refresh targeting. Play: Retarget Misdirected Effort."
|
|
14071
|
+
},
|
|
14072
|
+
aliases: ["signal to noise", "signal:noise", "s/n", "activity efficiency", "noise"]
|
|
14073
|
+
},
|
|
14074
|
+
{
|
|
14075
|
+
id: "thread_depth",
|
|
14076
|
+
kind: "vital",
|
|
14077
|
+
label: "Thread Depth",
|
|
14078
|
+
group: "Vital Signs",
|
|
14079
|
+
tagline: "How fragile is the pipeline if one champion goes dark?",
|
|
14080
|
+
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).",
|
|
14081
|
+
formula_lines: [
|
|
14082
|
+
"score = % open deals with \u22652 active people (90d)",
|
|
14083
|
+
"people counted via opp contacts + same-org activity",
|
|
14084
|
+
"threshold configurable (default 2)"
|
|
14085
|
+
],
|
|
14086
|
+
meaning: 'Board question: "how much revenue dies if one contact changes jobs?" Dollar value = sum of amount on single-threaded deals.',
|
|
14087
|
+
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.",
|
|
14088
|
+
deepdive: [
|
|
14089
|
+
"Status: green \u226565, yellow \u226540, red below 40.",
|
|
14090
|
+
'Dollar translation: sum of amount on single-threaded deals \u2192 "single-threaded".',
|
|
14091
|
+
"Layer 4 of the gating stack \u2014 read last, after the upstream vitals.",
|
|
14092
|
+
"Trigger play: Multi-Thread Your Deals (multi-thread-deals) when depth is low.",
|
|
14093
|
+
"Levers: buying-committee map, warm internal referral first, CRM contact roles, mid-stage single-thread alerts, champion job-change signals."
|
|
14094
|
+
],
|
|
14095
|
+
visual: {
|
|
14096
|
+
kind: "bars",
|
|
14097
|
+
caption: "Exemplar \u2014 multi-threaded vs single-threaded open deals",
|
|
14098
|
+
bars: [
|
|
14099
|
+
{ label: "Multi-threaded", value: 34, tone: "green" },
|
|
14100
|
+
{ label: "Single-threaded", value: 66, tone: "red" }
|
|
14101
|
+
]
|
|
14102
|
+
},
|
|
14103
|
+
play_id: "multi-thread-deals",
|
|
14104
|
+
dollar_label: "single-threaded",
|
|
14105
|
+
audience: {
|
|
14106
|
+
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.",
|
|
14107
|
+
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."
|
|
14108
|
+
},
|
|
14109
|
+
aliases: ["thread depth", "multithreading", "multi-thread", "single-threaded", "buying committee"]
|
|
14110
|
+
}
|
|
14111
|
+
];
|
|
14112
|
+
SAAS = [
|
|
14113
|
+
// —— Revenue ——
|
|
14114
|
+
{
|
|
14115
|
+
id: "arr",
|
|
14116
|
+
kind: "saas",
|
|
14117
|
+
label: "ARR",
|
|
14118
|
+
group: "Revenue",
|
|
14119
|
+
tagline: "How big is the revenue engine \u2014 and from where?",
|
|
14120
|
+
how_computed: "Sum of amount on closed-won opportunities in the dataset (pipeline-inferred ARR when a pure subscription ledger is unavailable).",
|
|
14121
|
+
formula_lines: [
|
|
14122
|
+
"ARR \u2248 \u03A3 amount on closed-won opportunities",
|
|
14123
|
+
"New + Expansion = growth \xB7 Churned + Contraction = leakage"
|
|
14124
|
+
],
|
|
14125
|
+
meaning: 'Board question: "how fast are we growing, and from where?" Always decompose growth into new vs expansion \u2014 the mix is the story.',
|
|
14126
|
+
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.",
|
|
14127
|
+
deepdive: [
|
|
14128
|
+
"Companion metrics: New ARR, Expansion ARR, Churned ARR, Contraction ARR.",
|
|
14129
|
+
"Estimation method may be ledger, pipeline_inferred, or snapshot \u2014 read confidence + reliability_gate.",
|
|
14130
|
+
"Cross-check with Freshness before trusting ARR growth stories built on zombie deals."
|
|
14131
|
+
],
|
|
14132
|
+
visual: {
|
|
14133
|
+
kind: "waterfall",
|
|
14134
|
+
caption: "Exemplar ARR walk \u2014 growth vs leakage",
|
|
14135
|
+
waterfall: [
|
|
14136
|
+
{ label: "Starting", delta: 100, cumulative: 100 },
|
|
14137
|
+
{ label: "+ New", delta: 18, cumulative: 118 },
|
|
14138
|
+
{ label: "+ Expansion", delta: 12, cumulative: 130 },
|
|
14139
|
+
{ label: "\u2212 Contraction", delta: -4, cumulative: 126 },
|
|
14140
|
+
{ label: "\u2212 Churned", delta: -8, cumulative: 118 }
|
|
14141
|
+
]
|
|
14142
|
+
},
|
|
14143
|
+
audience: {
|
|
14144
|
+
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.",
|
|
14145
|
+
ops: "Computed as \u03A3 closed-won amounts (pipeline-inferred when no ledger). Decompose into new / expansion / churned / contraction before briefing anyone."
|
|
14146
|
+
},
|
|
14147
|
+
aliases: ["annual recurring revenue", "revenue"]
|
|
14148
|
+
},
|
|
14149
|
+
{
|
|
14150
|
+
id: "new_arr",
|
|
14151
|
+
kind: "saas",
|
|
14152
|
+
label: "New ARR",
|
|
14153
|
+
group: "Revenue",
|
|
14154
|
+
tagline: "How much growth came from brand-new customers?",
|
|
14155
|
+
how_computed: "Closed-won tagged New Business, or first closed-won deal per organization when tags are missing.",
|
|
14156
|
+
formula_lines: [
|
|
14157
|
+
"New ARR = \u03A3 closed-won tagged New Business",
|
|
14158
|
+
"fallback: first closed-won deal per organization"
|
|
14159
|
+
],
|
|
14160
|
+
meaning: 'Board question: "is growth coming from the top of funnel, or are we farming the base?"',
|
|
14161
|
+
expert_read: "Rising New ARR with falling Expansion usually means land-and-expand is underpowered \u2014 packaging or CS motion, not just sales capacity.",
|
|
14162
|
+
deepdive: [
|
|
14163
|
+
"Pair with Expansion ARR \u2014 the mix tells you which motion is carrying growth.",
|
|
14164
|
+
"Tag quality matters: untagged deals fall into the first-deal-per-org heuristic."
|
|
14165
|
+
],
|
|
14166
|
+
visual: {
|
|
14167
|
+
kind: "bars",
|
|
14168
|
+
caption: "Exemplar growth mix",
|
|
14169
|
+
bars: [
|
|
14170
|
+
{ label: "New ARR", value: 60, tone: "accent" },
|
|
14171
|
+
{ label: "Expansion ARR", value: 40, tone: "green" }
|
|
14172
|
+
]
|
|
14173
|
+
},
|
|
14174
|
+
audience: {
|
|
14175
|
+
board: "New ARR is net-new logos. Read it next to Expansion \u2014 a healthy mix beats a one-sided engine.",
|
|
14176
|
+
ops: "Prefer CRM New Business tags; otherwise first closed-won per org. Watch tag hygiene."
|
|
14177
|
+
},
|
|
14178
|
+
aliases: ["new business arr", "new logo arr"]
|
|
14179
|
+
},
|
|
14180
|
+
{
|
|
14181
|
+
id: "expansion_arr",
|
|
14182
|
+
kind: "saas",
|
|
14183
|
+
label: "Expansion ARR",
|
|
14184
|
+
group: "Revenue",
|
|
14185
|
+
tagline: "How much are existing customers buying more?",
|
|
14186
|
+
how_computed: "Closed-won tagged Expansion, or later closed-won deals per organization after the first win.",
|
|
14187
|
+
formula_lines: [
|
|
14188
|
+
"Expansion ARR = \u03A3 closed-won tagged Expansion",
|
|
14189
|
+
"fallback: later closed-won deals per organization"
|
|
14190
|
+
],
|
|
14191
|
+
meaning: 'Board question: "is the installed base compounding?"',
|
|
14192
|
+
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.",
|
|
14193
|
+
deepdive: [
|
|
14194
|
+
"Feeds NRR as the upside term.",
|
|
14195
|
+
"Compare to Contraction \u2014 net expansion = expansion \u2212 contraction."
|
|
14196
|
+
],
|
|
14197
|
+
visual: {
|
|
14198
|
+
kind: "bars",
|
|
14199
|
+
caption: "Exemplar \u2014 expansion vs contraction",
|
|
14200
|
+
bars: [
|
|
14201
|
+
{ label: "Expansion", value: 70, tone: "green" },
|
|
14202
|
+
{ label: "Contraction", value: 25, tone: "yellow" }
|
|
14203
|
+
]
|
|
14204
|
+
},
|
|
14205
|
+
audience: {
|
|
14206
|
+
board: "Expansion ARR is installed-base compounding \u2014 the cheapest growth when it works.",
|
|
14207
|
+
ops: "Tagged Expansion or subsequent wins per org. Pair with Contraction before celebrating net expansion."
|
|
14208
|
+
},
|
|
14209
|
+
aliases: ["upsell", "upsell arr", "cross-sell"]
|
|
14210
|
+
},
|
|
14211
|
+
{
|
|
14212
|
+
id: "churned_arr",
|
|
14213
|
+
kind: "saas",
|
|
14214
|
+
label: "Churned ARR",
|
|
14215
|
+
group: "Revenue",
|
|
14216
|
+
tagline: "How much revenue walked out the door?",
|
|
14217
|
+
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.",
|
|
14218
|
+
formula_lines: [
|
|
14219
|
+
"Churned ARR = \u03A3 historical wins for orgs with",
|
|
14220
|
+
" no win in trailing 12mo AND no active open opp"
|
|
14221
|
+
],
|
|
14222
|
+
meaning: `Board question: "how leaky is the bucket before expansion papers over it?" (with Contraction, this is GRR's downside).`,
|
|
14223
|
+
expert_read: "Pipeline-inferred churn is a hypothesis \u2014 confirm with billing status when available. A spike often clusters in one segment or cohort.",
|
|
14224
|
+
deepdive: [
|
|
14225
|
+
"Feeds GRR and NRR as the churn term.",
|
|
14226
|
+
"Cut by segment / motion before treating it as a company-wide PMF problem."
|
|
14227
|
+
],
|
|
14228
|
+
visual: {
|
|
14229
|
+
kind: "bars",
|
|
14230
|
+
caption: "Exemplar leakage mix",
|
|
14231
|
+
bars: [
|
|
14232
|
+
{ label: "Churned", value: 55, tone: "red" },
|
|
14233
|
+
{ label: "Contraction", value: 30, tone: "yellow" }
|
|
14234
|
+
]
|
|
14235
|
+
},
|
|
14236
|
+
audience: {
|
|
14237
|
+
board: "Churned ARR is full logo loss. With Contraction it sets the floor of the business (GRR).",
|
|
14238
|
+
ops: "Heuristic: historical winners with no trailing-12 win and no open opp. Validate against billing when you can."
|
|
14239
|
+
},
|
|
14240
|
+
aliases: ["churn", "logo churn", "churned revenue"]
|
|
14241
|
+
},
|
|
14242
|
+
{
|
|
14243
|
+
id: "contraction_arr",
|
|
14244
|
+
kind: "saas",
|
|
14245
|
+
label: "Contraction ARR",
|
|
14246
|
+
group: "Revenue",
|
|
14247
|
+
tagline: "How much did existing customers buy less?",
|
|
14248
|
+
how_computed: "Organizations with \u22652 wins where the latest amount is less than the prior \u2014 sum of the negative deltas.",
|
|
14249
|
+
formula_lines: [
|
|
14250
|
+
"Contraction = \u03A3 (prior \u2212 latest) where latest < prior",
|
|
14251
|
+
"requires \u22652 closed-won deals per organization"
|
|
14252
|
+
],
|
|
14253
|
+
meaning: 'Board question: "are we quietly shrinking inside the base while logos stay?"',
|
|
14254
|
+
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.",
|
|
14255
|
+
deepdive: [
|
|
14256
|
+
"Feeds GRR and NRR.",
|
|
14257
|
+
"Needs multi-deal history per org \u2014 thin history understates contraction."
|
|
14258
|
+
],
|
|
14259
|
+
visual: {
|
|
14260
|
+
kind: "waterfall",
|
|
14261
|
+
caption: "Exemplar \u2014 contraction digs into the base",
|
|
14262
|
+
waterfall: [
|
|
14263
|
+
{ label: "Prior", delta: 100, cumulative: 100 },
|
|
14264
|
+
{ label: "Latest", delta: -18, cumulative: 82 }
|
|
14265
|
+
]
|
|
14266
|
+
},
|
|
14267
|
+
audience: {
|
|
14268
|
+
board: "Contraction is silent shrink inside retained logos \u2014 often packaging or seats, not a cancelled contract.",
|
|
14269
|
+
ops: "Requires \u22652 wins per org with a down-round. Pair with Expansion for net expansion."
|
|
14270
|
+
},
|
|
14271
|
+
aliases: ["downgrade", "seat reduction", "contraction"]
|
|
14272
|
+
},
|
|
14273
|
+
// —— Retention ——
|
|
14274
|
+
{
|
|
14275
|
+
id: "nrr",
|
|
14276
|
+
kind: "saas",
|
|
14277
|
+
label: "Net Revenue Retention",
|
|
14278
|
+
group: "Retention",
|
|
14279
|
+
tagline: "Would this business grow if sales stopped selling?",
|
|
14280
|
+
how_computed: "startingArr = ARR + Churned + Contraction \u2212 Expansion; NRR = ((starting \u2212 Churned \u2212 Contraction + Expansion) / starting) \xD7 100.",
|
|
14281
|
+
formula_lines: [
|
|
14282
|
+
"starting = ARR + churned + contraction \u2212 expansion",
|
|
14283
|
+
"NRR = (starting \u2212 churned \u2212 contraction + expansion) / starting \xD7 100",
|
|
14284
|
+
"NRR = 100% + expansion% \u2212 contraction% \u2212 churn%"
|
|
14285
|
+
],
|
|
14286
|
+
meaning: 'Board question: "would this business grow if sales stopped selling?" >100% means growing from existing customers.',
|
|
14287
|
+
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.",
|
|
14288
|
+
deepdive: [
|
|
14289
|
+
"Always show the waterfall: +expansion \u2212contraction \u2212churn.",
|
|
14290
|
+
"GRR is the floor; NRR adds expansion on top.",
|
|
14291
|
+
"On pipeline-only data, treat as a hypothesis \u2014 check confidence / reliability_gate."
|
|
14292
|
+
],
|
|
14293
|
+
visual: {
|
|
14294
|
+
kind: "waterfall",
|
|
14295
|
+
caption: "Exemplar NRR walk from 100%",
|
|
14296
|
+
waterfall: [
|
|
14297
|
+
{ label: "100%", delta: 100, cumulative: 100 },
|
|
14298
|
+
{ label: "+ Expansion", delta: 14, cumulative: 114 },
|
|
14299
|
+
{ label: "\u2212 Contraction", delta: -4, cumulative: 110 },
|
|
14300
|
+
{ label: "\u2212 Churn", delta: -6, cumulative: 104 }
|
|
14301
|
+
]
|
|
14302
|
+
},
|
|
14303
|
+
audience: {
|
|
14304
|
+
board: "NRR >100% means the base compounds without new logos. Decompose before judging \u2014 same number, different owners.",
|
|
14305
|
+
ops: "NRR = 100 + expansion \u2212 contraction \u2212 churn. Motion benchmarks calibrate green/yellow bands. Check reliability_gate on pipeline-inferred data."
|
|
14306
|
+
},
|
|
14307
|
+
aliases: ["net revenue retention", "net retention", "ndr"],
|
|
14308
|
+
benchmarkHint: (motion) => pctBand("nrr", motion)
|
|
14309
|
+
},
|
|
14310
|
+
{
|
|
14311
|
+
id: "grr",
|
|
14312
|
+
kind: "saas",
|
|
14313
|
+
label: "Gross Revenue Retention",
|
|
14314
|
+
group: "Retention",
|
|
14315
|
+
tagline: "How leaky is the bucket before expansion papers over it?",
|
|
14316
|
+
how_computed: "GRR = ((startingArr \u2212 Churned \u2212 Contraction) / startingArr) \xD7 100 \u2014 expansion is excluded on purpose.",
|
|
14317
|
+
formula_lines: [
|
|
14318
|
+
"starting = ARR + churned + contraction \u2212 expansion",
|
|
14319
|
+
"GRR = (starting \u2212 churned \u2212 contraction) / starting \xD7 100"
|
|
14320
|
+
],
|
|
14321
|
+
meaning: 'Board question: "how leaky is the bucket before expansion papers over it?" Prior: >90% healthy, >95% strong for enterprise.',
|
|
14322
|
+
expert_read: "GRR is the honesty metric. Expansion can make NRR look fine while GRR is quietly eroding \u2014 always read both.",
|
|
14323
|
+
deepdive: [
|
|
14324
|
+
"GRR never includes Expansion \u2014 that is the point.",
|
|
14325
|
+
"Owners: product/CS for churn, packaging for contraction."
|
|
14326
|
+
],
|
|
14327
|
+
visual: {
|
|
14328
|
+
kind: "gauge",
|
|
14329
|
+
caption: "Exemplar GRR \u2014 floor of the business",
|
|
14330
|
+
gauge: 92
|
|
14331
|
+
},
|
|
14332
|
+
audience: {
|
|
14333
|
+
board: "GRR is the floor \u2014 churn + contraction only. Expansion cannot paper over a leaky bucket here.",
|
|
14334
|
+
ops: "Exclude Expansion by design. Pair with NRR; diagnose churn vs contraction separately."
|
|
14335
|
+
},
|
|
14336
|
+
aliases: ["gross revenue retention", "gross retention"],
|
|
14337
|
+
benchmarkHint: (motion) => pctBand("grr", motion)
|
|
14338
|
+
},
|
|
14339
|
+
// —— Pipeline ——
|
|
14340
|
+
{
|
|
14341
|
+
id: "pipeline_coverage",
|
|
14342
|
+
kind: "saas",
|
|
14343
|
+
label: "Pipeline Coverage",
|
|
14344
|
+
group: "Pipeline",
|
|
14345
|
+
tagline: "Is next quarter already at risk?",
|
|
14346
|
+
how_computed: "Open pipeline amount \xF7 trailing-90-day closed-won amount.",
|
|
14347
|
+
formula_lines: [
|
|
14348
|
+
"Coverage = openPipeline / trailing_90d_won",
|
|
14349
|
+
"required \u2248 1 / win_rate (discount for time left)"
|
|
14350
|
+
],
|
|
14351
|
+
meaning: 'Board question: "is next quarter already at risk?" Priors scale with cycle length: ~3x velocity/SMB, 4\u20135x enterprise.',
|
|
14352
|
+
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.",
|
|
14353
|
+
deepdive: [
|
|
14354
|
+
"Always pair with Win Rate and Freshness.",
|
|
14355
|
+
"Weighted Pipeline is the credibility-adjusted cousin."
|
|
14356
|
+
],
|
|
14357
|
+
visual: {
|
|
14358
|
+
kind: "gauge",
|
|
14359
|
+
caption: "Exemplar coverage vs a 3x target",
|
|
14360
|
+
gauge: 72,
|
|
14361
|
+
bars: [
|
|
14362
|
+
{ label: "Open pipeline", value: 75, tone: "accent" },
|
|
14363
|
+
{ label: "Trailing won (scaled)", value: 25, tone: "neutral" }
|
|
14364
|
+
]
|
|
14365
|
+
},
|
|
14366
|
+
audience: {
|
|
14367
|
+
board: "Coverage answers whether next quarter is already under-piped. Fake coverage from zombies is worse than an honest gap.",
|
|
14368
|
+
ops: "open / trailing-90d won. Required \u2248 1/win_rate. Cross-check Freshness before briefing."
|
|
14369
|
+
},
|
|
14370
|
+
aliases: ["coverage", "pipeline coverage", "pipe coverage"],
|
|
14371
|
+
benchmarkHint: (motion) => pctBand("pipeline_coverage", motion)
|
|
14372
|
+
},
|
|
14373
|
+
{
|
|
14374
|
+
id: "weighted_pipeline",
|
|
14375
|
+
kind: "saas",
|
|
14376
|
+
label: "Weighted Pipeline",
|
|
14377
|
+
group: "Pipeline",
|
|
14378
|
+
tagline: "What is the pipeline worth after stage probability?",
|
|
14379
|
+
how_computed: "Sum of amount \xD7 stage probability for open deals (CRM Probability when present, else stage defaults).",
|
|
14380
|
+
formula_lines: [
|
|
14381
|
+
"Weighted = \u03A3 (amount \xD7 stageProbability)",
|
|
14382
|
+
"trust \u2264 stage discipline deserves"
|
|
14383
|
+
],
|
|
14384
|
+
meaning: 'Board question: "what should we actually forecast from open pipe?"',
|
|
14385
|
+
expert_read: "Trust it only as much as stage discipline deserves. Inflated late stages make weighted pipeline a fiction.",
|
|
14386
|
+
deepdive: [
|
|
14387
|
+
"Compare to unweighted open pipeline \u2014 a huge gap means optimistic stages.",
|
|
14388
|
+
"Pair with Flow Rate (stuck late stages)."
|
|
14389
|
+
],
|
|
14390
|
+
visual: {
|
|
14391
|
+
kind: "bars",
|
|
14392
|
+
caption: "Exemplar \u2014 open vs weighted",
|
|
14393
|
+
bars: [
|
|
14394
|
+
{ label: "Open pipeline", value: 100, tone: "neutral" },
|
|
14395
|
+
{ label: "Weighted", value: 42, tone: "accent" }
|
|
14396
|
+
]
|
|
14397
|
+
},
|
|
14398
|
+
audience: {
|
|
14399
|
+
board: "Weighted Pipeline is the credibility-adjusted forecast input \u2014 only as good as stage discipline.",
|
|
14400
|
+
ops: "\u03A3 amount \xD7 probability. Audit stage probabilities when weighted << open."
|
|
14401
|
+
},
|
|
14402
|
+
aliases: ["weighted pipe", "probability-weighted pipeline"]
|
|
14403
|
+
},
|
|
14404
|
+
{
|
|
14405
|
+
id: "pipeline_created",
|
|
14406
|
+
kind: "saas",
|
|
14407
|
+
label: "Pipeline Created (90d)",
|
|
14408
|
+
group: "Pipeline",
|
|
14409
|
+
tagline: "How much new pipe did we generate recently?",
|
|
14410
|
+
how_computed: "Sum of amounts for opportunities created in the last 90 days.",
|
|
14411
|
+
formula_lines: ["Pipeline Created = \u03A3 amount where created_at within 90d"],
|
|
14412
|
+
meaning: 'Board question: "is the top of funnel still filling?"',
|
|
14413
|
+
expert_read: "Falling created pipeline with flat coverage is a future miss \u2014 coverage is lagging; created is leading.",
|
|
14414
|
+
deepdive: [
|
|
14415
|
+
"Leading indicator for next-quarter coverage.",
|
|
14416
|
+
"Cut by source / segment to find where creation stalled."
|
|
14417
|
+
],
|
|
14418
|
+
visual: {
|
|
14419
|
+
kind: "bars",
|
|
14420
|
+
caption: "Exemplar \u2014 created vs needed",
|
|
14421
|
+
bars: [
|
|
14422
|
+
{ label: "Created (90d)", value: 55, tone: "yellow" },
|
|
14423
|
+
{ label: "Target pace", value: 80, tone: "green" }
|
|
14424
|
+
]
|
|
14425
|
+
},
|
|
14426
|
+
audience: {
|
|
14427
|
+
board: "Pipeline Created is a leading indicator \u2014 coverage lagging means the miss is already in motion.",
|
|
14428
|
+
ops: "\u03A3 amounts on opps created in 90d. Cut by source when it dips."
|
|
14429
|
+
},
|
|
14430
|
+
aliases: ["pipe gen", "pipeline generation", "created pipeline"]
|
|
14431
|
+
},
|
|
14432
|
+
{
|
|
14433
|
+
id: "pipeline_velocity",
|
|
14434
|
+
kind: "saas",
|
|
14435
|
+
label: "Pipeline Velocity",
|
|
14436
|
+
group: "Pipeline",
|
|
14437
|
+
tagline: "Revenue throughput per day \u2014 four levers, one number.",
|
|
14438
|
+
how_computed: "(openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays \u2014 requires \u22653 dated closed-won deals. Unit: $/day.",
|
|
14439
|
+
formula_lines: [
|
|
14440
|
+
"Velocity = (openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays",
|
|
14441
|
+
"four levers: #opps \xB7 deal size \xB7 win rate \xB7 cycle days"
|
|
14442
|
+
],
|
|
14443
|
+
meaning: 'Board question: "which lever moved when throughput changed?" The most decision-ready pipeline metric.',
|
|
14444
|
+
expert_read: "When velocity changes, name WHICH lever moved. A win-rate rise on falling opp volume is qualification tightening, not improvement.",
|
|
14445
|
+
deepdive: [
|
|
14446
|
+
"Needs \u22653 dated wins \u2014 otherwise unavailable.",
|
|
14447
|
+
"Pairs with Flow Rate (cycle) and Win Rate (conversion)."
|
|
14448
|
+
],
|
|
14449
|
+
visual: {
|
|
14450
|
+
kind: "levers",
|
|
14451
|
+
caption: "Four levers \u2014 say which one moved",
|
|
14452
|
+
levers: ["# Open opps", "Avg deal size", "Win rate", "Cycle days"]
|
|
14453
|
+
},
|
|
14454
|
+
audience: {
|
|
14455
|
+
board: "Velocity is throughput. When it moves, demand the lever \u2014 volume, size, win rate, or cycle \u2014 not a shrug.",
|
|
14456
|
+
ops: "(opps \xD7 avgDeal \xD7 winRate) / cycleDays. Diagnose the moved lever before prescribing."
|
|
14457
|
+
},
|
|
14458
|
+
aliases: ["velocity", "pipeline velocity", "throughput"]
|
|
14459
|
+
},
|
|
14460
|
+
// —— Sales efficiency ——
|
|
14461
|
+
{
|
|
14462
|
+
id: "win_rate",
|
|
14463
|
+
kind: "saas",
|
|
14464
|
+
label: "Win Rate",
|
|
14465
|
+
group: "Sales Efficiency",
|
|
14466
|
+
tagline: "Of decided deals, how often do we win?",
|
|
14467
|
+
how_computed: "closed-won / (won + lost) \xD7 100.",
|
|
14468
|
+
formula_lines: ["Win Rate = won / (won + lost) \xD7 100"],
|
|
14469
|
+
meaning: 'Board question: "are we converting the pipe we create?" Priors: 25\u201335% SMB, 18\u201325% mid-market, 12\u201318% enterprise on qualified opps.',
|
|
14470
|
+
expert_read: "A rising win rate on falling opp volume is qualification tightening, not improvement \u2014 check the denominator.",
|
|
14471
|
+
deepdive: [
|
|
14472
|
+
"Required coverage \u2248 1 / win rate.",
|
|
14473
|
+
"Cut by segment / source before company-wide coaching."
|
|
14474
|
+
],
|
|
14475
|
+
visual: {
|
|
14476
|
+
kind: "split",
|
|
14477
|
+
caption: "Exemplar decided deals",
|
|
14478
|
+
bars: [
|
|
14479
|
+
{ label: "Won", value: 28, tone: "green" },
|
|
14480
|
+
{ label: "Lost", value: 72, tone: "red" }
|
|
14481
|
+
]
|
|
14482
|
+
},
|
|
14483
|
+
audience: {
|
|
14484
|
+
board: "Win Rate is conversion of decided deals. Rising win rate with falling volume is often tighter qualification, not better selling.",
|
|
14485
|
+
ops: "won/(won+lost). Check the denominator. Motion benchmarks set green/yellow bands."
|
|
14486
|
+
},
|
|
14487
|
+
aliases: ["close rate", "winrate", "win %"],
|
|
14488
|
+
benchmarkHint: (motion) => pctBand("win_rate", motion)
|
|
14489
|
+
},
|
|
14490
|
+
{
|
|
14491
|
+
id: "avg_deal_size",
|
|
14492
|
+
kind: "saas",
|
|
14493
|
+
label: "Avg Deal Size",
|
|
14494
|
+
group: "Sales Efficiency",
|
|
14495
|
+
tagline: "What does a typical win look like?",
|
|
14496
|
+
how_computed: "Mean amount on closed-won opportunities.",
|
|
14497
|
+
formula_lines: ["Avg Deal = mean(closed-won amount)"],
|
|
14498
|
+
meaning: 'Board question: "are we selling the motion we think we are?"',
|
|
14499
|
+
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.",
|
|
14500
|
+
deepdive: [
|
|
14501
|
+
"Feeds Pipeline Velocity and LTV proxy.",
|
|
14502
|
+
"Cut by segment \u2014 averages hide bimodal motions."
|
|
14503
|
+
],
|
|
14504
|
+
visual: {
|
|
14505
|
+
kind: "bars",
|
|
14506
|
+
caption: "Exemplar \u2014 size mix by segment",
|
|
14507
|
+
bars: [
|
|
14508
|
+
{ label: "SMB", value: 30, tone: "neutral" },
|
|
14509
|
+
{ label: "Mid-market", value: 55, tone: "accent" },
|
|
14510
|
+
{ label: "Enterprise", value: 90, tone: "green" }
|
|
14511
|
+
]
|
|
14512
|
+
},
|
|
14513
|
+
audience: {
|
|
14514
|
+
board: "Avg Deal Size should match the motion you claim. Mix shift changes coverage and capacity math.",
|
|
14515
|
+
ops: "Mean closed-won amount. Segment before coaching on size."
|
|
14516
|
+
},
|
|
14517
|
+
aliases: ["average deal size", "asp", "acv"]
|
|
14518
|
+
},
|
|
14519
|
+
{
|
|
14520
|
+
id: "avg_sales_cycle",
|
|
14521
|
+
kind: "saas",
|
|
14522
|
+
label: "Avg Sales Cycle",
|
|
14523
|
+
group: "Sales Efficiency",
|
|
14524
|
+
tagline: "How long from create to close on wins?",
|
|
14525
|
+
how_computed: "Mean days from created_at to close date on dated closed-won deals.",
|
|
14526
|
+
formula_lines: ["Avg Cycle = mean(close_date \u2212 created_at) on dated wins"],
|
|
14527
|
+
meaning: 'Board question: "is the cycle stretching \u2014 the earliest soft signal of deal-quality decay?"',
|
|
14528
|
+
expert_read: "Cycle stretching past the motion's norm is the earliest soft signal of deal-quality decay. Pair with Flow Rate stuck stages.",
|
|
14529
|
+
deepdive: [
|
|
14530
|
+
"Feeds Pipeline Velocity as the denominator.",
|
|
14531
|
+
"Needs dated wins \u2014 missing close dates understate/omit."
|
|
14532
|
+
],
|
|
14533
|
+
visual: {
|
|
14534
|
+
kind: "bars",
|
|
14535
|
+
caption: "Exemplar cycle vs motion norm",
|
|
14536
|
+
bars: [
|
|
14537
|
+
{ label: "Your cycle", value: 78, tone: "yellow" },
|
|
14538
|
+
{ label: "Motion norm", value: 55, tone: "green" }
|
|
14539
|
+
]
|
|
14540
|
+
},
|
|
14541
|
+
audience: {
|
|
14542
|
+
board: "Cycle stretch is an early soft signal that quality or process is slipping \u2014 before the miss shows in bookings.",
|
|
14543
|
+
ops: "Mean create\u2192close on dated wins. Investigate the stage that aged."
|
|
14544
|
+
},
|
|
14545
|
+
aliases: ["sales cycle", "cycle length", "time to close"]
|
|
14546
|
+
},
|
|
14547
|
+
{
|
|
14548
|
+
id: "stage_conversion",
|
|
14549
|
+
kind: "saas",
|
|
14550
|
+
label: "Stage Conversion",
|
|
14551
|
+
group: "Sales Efficiency",
|
|
14552
|
+
tagline: "Where in the stage model does advancement collapse?",
|
|
14553
|
+
how_computed: "From metadata.stage_history stage advances when present; otherwise a win-rate proxy.",
|
|
14554
|
+
formula_lines: [
|
|
14555
|
+
"Preferred: advancement rates from stage_history",
|
|
14556
|
+
"Fallback: win-rate proxy when history is missing"
|
|
14557
|
+
],
|
|
14558
|
+
meaning: 'Board question: "which single stage is starving everything downstream?"',
|
|
14559
|
+
expert_read: "Find the one stage where conversion collapses \u2014 that's the process problem; everything downstream is starvation.",
|
|
14560
|
+
deepdive: [
|
|
14561
|
+
"Best with stage_history metadata; otherwise treat as proxy.",
|
|
14562
|
+
"Pairs with Flow Rate stage-age cuts."
|
|
14563
|
+
],
|
|
14564
|
+
visual: {
|
|
14565
|
+
kind: "funnel",
|
|
14566
|
+
caption: "Exemplar \u2014 find the collapse",
|
|
14567
|
+
funnel: [
|
|
14568
|
+
{ label: "Stage 1\u21922", widthPct: 100 },
|
|
14569
|
+
{ label: "Stage 2\u21923", widthPct: 72 },
|
|
14570
|
+
{ label: "Stage 3\u21924", widthPct: 28 },
|
|
14571
|
+
{ label: "Stage 4\u2192Close", widthPct: 18 }
|
|
14572
|
+
]
|
|
14573
|
+
},
|
|
14574
|
+
audience: {
|
|
14575
|
+
board: "Stage Conversion names the bottleneck stage \u2014 one collapse starves every stage after it.",
|
|
14576
|
+
ops: "Prefer stage_history advances. Fix the collapse stage before coaching downstream reps."
|
|
14577
|
+
},
|
|
14578
|
+
aliases: ["stage conversion", "stage advance", "conversion by stage"]
|
|
14579
|
+
},
|
|
14580
|
+
// —— Unit economics ——
|
|
14581
|
+
{
|
|
14582
|
+
id: "ltv_proxy",
|
|
14583
|
+
kind: "saas",
|
|
14584
|
+
label: "LTV (Proxy)",
|
|
14585
|
+
group: "Unit Economics",
|
|
14586
|
+
tagline: "Rough lifetime value from deal size and GRR.",
|
|
14587
|
+
how_computed: "avgDeal / ((100 \u2212 GRR) / 100) when GRR < 100. Unavailable when GRR is 100%+ or missing.",
|
|
14588
|
+
formula_lines: [
|
|
14589
|
+
"LTV \u2248 avgDeal / churnRate",
|
|
14590
|
+
"churnRate = (100 \u2212 GRR) / 100 (requires GRR < 100)"
|
|
14591
|
+
],
|
|
14592
|
+
meaning: 'Board question: "what is a customer roughly worth over their life?"',
|
|
14593
|
+
expert_read: "This is a proxy \u2014 not a cohort LTV. Use it for direction, not capital allocation.",
|
|
14594
|
+
deepdive: [
|
|
14595
|
+
"Unavailable when GRR \u2265 100 or missing.",
|
|
14596
|
+
"Pairs with CAC for LTV:CAC when spend data exists."
|
|
14597
|
+
],
|
|
14598
|
+
visual: {
|
|
14599
|
+
kind: "gauge",
|
|
14600
|
+
caption: "Exemplar LTV proxy (directional)",
|
|
14601
|
+
gauge: 68
|
|
14602
|
+
},
|
|
14603
|
+
audience: {
|
|
14604
|
+
board: "LTV Proxy is directional from deal size and GRR \u2014 not a cohort LTV. Use for orientation, not capital decisions.",
|
|
14605
|
+
ops: "avgDeal / ((100\u2212GRR)/100). Needs GRR < 100. Prefer cohort math when billing data arrives."
|
|
14606
|
+
},
|
|
14607
|
+
aliases: ["ltv", "lifetime value"]
|
|
14608
|
+
},
|
|
14609
|
+
{
|
|
14610
|
+
id: "cac",
|
|
14611
|
+
kind: "saas",
|
|
14612
|
+
label: "CAC",
|
|
14613
|
+
group: "Unit Economics",
|
|
14614
|
+
tagline: "Customer acquisition cost \u2014 needs spend data.",
|
|
14615
|
+
how_computed: "Requires campaign / sales spend data. Currently unavailable on CRM-only datasets.",
|
|
14616
|
+
formula_lines: ["CAC = sales & marketing spend / new customers", "(requires spend data \u2014 not in CRM-only exports)"],
|
|
14617
|
+
meaning: 'Board question: "what does a new logo cost to win?"',
|
|
14618
|
+
expert_read: "Without spend, NTRP cannot invent CAC. Wire campaign spend or finance exports to unlock unit economics.",
|
|
14619
|
+
deepdive: [
|
|
14620
|
+
"Always unavailable on CRM-only demos \u2014 expected.",
|
|
14621
|
+
"Unlocks LTV:CAC, Payback, Magic Number when spend lands."
|
|
14622
|
+
],
|
|
14623
|
+
visual: { kind: "none", caption: "Needs campaign spend / finance export" },
|
|
14624
|
+
audience: {
|
|
14625
|
+
board: "CAC is locked until spend data is connected \u2014 CRM alone cannot price acquisition.",
|
|
14626
|
+
ops: "Bring campaign or S&M spend. Until then unit-econ metrics stay unavailable by design."
|
|
14627
|
+
},
|
|
14628
|
+
aliases: ["customer acquisition cost", "acquisition cost"]
|
|
14629
|
+
},
|
|
14630
|
+
{
|
|
14631
|
+
id: "ltv_cac_ratio",
|
|
14632
|
+
kind: "saas",
|
|
14633
|
+
label: "LTV:CAC Ratio",
|
|
14634
|
+
group: "Unit Economics",
|
|
14635
|
+
tagline: "Is acquisition spend earning its keep?",
|
|
14636
|
+
how_computed: "LTV proxy \xF7 CAC. Unavailable without spend (CAC).",
|
|
14637
|
+
formula_lines: ["LTV:CAC = LTV_proxy / CAC", "(requires CAC)"],
|
|
14638
|
+
meaning: 'Board question: "do we earn enough lifetime value per dollar spent to acquire?"',
|
|
14639
|
+
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.",
|
|
14640
|
+
deepdive: ["Blocked on CAC. See LTV Proxy and CAC."],
|
|
14641
|
+
visual: { kind: "none", caption: "Needs CAC (spend data)" },
|
|
14642
|
+
audience: {
|
|
14643
|
+
board: "LTV:CAC is the acquisition ROI story \u2014 available once spend is wired.",
|
|
14644
|
+
ops: "LTV_proxy / CAC. Unlocks with spend import."
|
|
14645
|
+
},
|
|
14646
|
+
aliases: ["ltv cac", "ltv/cac", "ltv to cac"]
|
|
14647
|
+
},
|
|
14648
|
+
{
|
|
14649
|
+
id: "payback_months",
|
|
14650
|
+
kind: "saas",
|
|
14651
|
+
label: "Payback Months",
|
|
14652
|
+
group: "Unit Economics",
|
|
14653
|
+
tagline: "How many months to recover CAC?",
|
|
14654
|
+
how_computed: "Requires CAC / spend. Lower is better.",
|
|
14655
|
+
formula_lines: ["Payback \u2248 CAC / (monthly gross profit per customer)", "(requires spend data)"],
|
|
14656
|
+
meaning: 'Board question: "how fast does acquisition spend return?" Efficiency era prior: <18 months often healthy.',
|
|
14657
|
+
expert_read: "Boards now weigh payback (<18mo) as heavily as growth in many motions.",
|
|
14658
|
+
deepdive: ["Blocked on CAC. Benchmarks exist per motion once data lands."],
|
|
14659
|
+
visual: { kind: "none", caption: "Needs CAC (spend data)" },
|
|
14660
|
+
audience: {
|
|
14661
|
+
board: "Payback is how fast CAC returns. Efficiency-era boards often want <18 months.",
|
|
14662
|
+
ops: "Requires CAC. Motion green/yellow bands apply when available."
|
|
14663
|
+
},
|
|
14664
|
+
aliases: ["payback", "cac payback"],
|
|
14665
|
+
benchmarkHint: (motion) => monthsBand(motion)
|
|
14666
|
+
},
|
|
14667
|
+
{
|
|
14668
|
+
id: "magic_number",
|
|
14669
|
+
kind: "saas",
|
|
14670
|
+
label: "Magic Number",
|
|
14671
|
+
group: "Unit Economics",
|
|
14672
|
+
tagline: "Sales efficiency \u2014 net new ARR per sales dollar.",
|
|
14673
|
+
how_computed: "Requires sales spend. Classic form: net new ARR (quarter) / prior-quarter S&M spend.",
|
|
14674
|
+
formula_lines: [
|
|
14675
|
+
"Magic Number \u2248 Net New ARR(q) / S&M spend(q\u22121)",
|
|
14676
|
+
"(requires spend data)"
|
|
14677
|
+
],
|
|
14678
|
+
meaning: 'Board question: "how efficiently does sales spend produce net new ARR?" Prior: >0.75 often healthy; >1 strong.',
|
|
14679
|
+
expert_read: "Efficiency era: magic number >0.75 is weighed alongside growth. Without spend, NTRP keeps this unavailable rather than inventing it.",
|
|
14680
|
+
deepdive: ["Blocked on spend. Benchmarks per motion ready when data lands."],
|
|
14681
|
+
visual: { kind: "none", caption: "Needs S&M spend data" },
|
|
14682
|
+
audience: {
|
|
14683
|
+
board: "Magic Number prices sales efficiency. Available once S&M spend is connected.",
|
|
14684
|
+
ops: "Net new ARR / prior S&M. Motion benchmarks apply when spend lands."
|
|
14685
|
+
},
|
|
14686
|
+
aliases: ["sales magic number", "sales efficiency magic number"],
|
|
14687
|
+
benchmarkHint: (motion) => magicBand(motion)
|
|
14688
|
+
}
|
|
14689
|
+
];
|
|
14690
|
+
METRIC_DEFINITIONS = [...VITALS, ...SAAS];
|
|
14691
|
+
BY_ID = new Map(METRIC_DEFINITIONS.map((m) => [m.id, m]));
|
|
14692
|
+
ALIAS_INDEX = (() => {
|
|
14693
|
+
const idx = /* @__PURE__ */ new Map();
|
|
14694
|
+
for (const m of METRIC_DEFINITIONS) {
|
|
14695
|
+
idx.set(m.id.toLowerCase(), m.id);
|
|
14696
|
+
idx.set(m.label.toLowerCase(), m.id);
|
|
14697
|
+
for (const a of m.aliases ?? []) {
|
|
14698
|
+
idx.set(a.toLowerCase(), m.id);
|
|
14699
|
+
}
|
|
14700
|
+
}
|
|
14701
|
+
idx.set("signal-to-noise", "signal_to_noise");
|
|
14702
|
+
idx.set("signal:noise", "signal_to_noise");
|
|
14703
|
+
idx.set("flow-rate", "flow_rate");
|
|
14704
|
+
idx.set("drop-rate", "drop_rate");
|
|
14705
|
+
idx.set("thread-depth", "thread_depth");
|
|
14706
|
+
return idx;
|
|
14707
|
+
})();
|
|
14708
|
+
SAAS_METRIC_IDS = SAAS.map((m) => m.id);
|
|
14709
|
+
}
|
|
14710
|
+
});
|
|
14711
|
+
|
|
14712
|
+
// src/services/metric-explainers.ts
|
|
14713
|
+
function normalizeAudience(audience) {
|
|
14714
|
+
if (!audience) return "board";
|
|
14715
|
+
const a = String(audience).toLowerCase();
|
|
14716
|
+
if (a === "ops" || a === "operations" || a === "operator" || a === "team") {
|
|
14717
|
+
return "ops";
|
|
14718
|
+
}
|
|
14719
|
+
return "board";
|
|
13545
14720
|
}
|
|
13546
|
-
function
|
|
13547
|
-
return
|
|
14721
|
+
function audienceLabel(audience) {
|
|
14722
|
+
return audience === "ops" ? "ops" : "board / exec";
|
|
13548
14723
|
}
|
|
13549
|
-
function
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
return `- ${label}: ${formatted}${confStr}`;
|
|
14724
|
+
function statusRankFrom(status) {
|
|
14725
|
+
if (status === "red") return 0;
|
|
14726
|
+
if (status === "yellow") return 1;
|
|
14727
|
+
if (status === "green") return 2;
|
|
14728
|
+
return 3;
|
|
13555
14729
|
}
|
|
13556
|
-
function
|
|
13557
|
-
const
|
|
13558
|
-
const
|
|
13559
|
-
const
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
if (
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
const counts = ctx.dataset.counts ?? {};
|
|
13567
|
-
const countStr = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k}`).join(", ");
|
|
13568
|
-
lines.push(`Dataset: ${ctx.dataset.label}${countStr ? ` (${countStr})` : ""}`);
|
|
13569
|
-
}
|
|
13570
|
-
const completed = ctx.analysis.completed;
|
|
13571
|
-
if (completed.length > 0) {
|
|
13572
|
-
lines.push(`Analysis lenses completed: ${completed.join(", ")}`);
|
|
13573
|
-
}
|
|
13574
|
-
lines.push("");
|
|
13575
|
-
if (diagnosis) {
|
|
13576
|
-
const { health, findings } = diagnosis;
|
|
13577
|
-
lines.push("## GTM health (vital signs)");
|
|
13578
|
-
lines.push(`Overall score: ${Math.round(health.overall_score)} (${health.overall_status})`);
|
|
13579
|
-
if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
|
|
13580
|
-
lines.push(`Total value at risk: ${formatCurrency(health.total_value_at_risk)}`);
|
|
13581
|
-
}
|
|
13582
|
-
lines.push("");
|
|
13583
|
-
lines.push("### Vital signs");
|
|
13584
|
-
for (const vs of health.vital_signs) {
|
|
13585
|
-
const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
|
|
13586
|
-
const dollars = vs.dollar_value != null ? ` \u2014 ${formatCurrency(vs.dollar_value)}${vs.dollar_label ? ` ${vs.dollar_label}` : ""}` : "";
|
|
13587
|
-
lines.push(`- ${label}: ${Math.round(vs.score)} (${vs.status})${dollars}`);
|
|
14730
|
+
function collectCandidates(bundle, opts) {
|
|
14731
|
+
const prefer = new Set(opts.prefer ?? []);
|
|
14732
|
+
const map = /* @__PURE__ */ new Map();
|
|
14733
|
+
const upsert = (id, priority, status) => {
|
|
14734
|
+
if (!getMetricExplainer(id)) return;
|
|
14735
|
+
const existing = map.get(id);
|
|
14736
|
+
const rank = statusRankFrom(status);
|
|
14737
|
+
if (!existing) {
|
|
14738
|
+
map.set(id, { id, priority, statusRank: rank });
|
|
14739
|
+
return;
|
|
13588
14740
|
}
|
|
13589
|
-
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
|
|
14741
|
+
existing.priority = Math.min(existing.priority, priority);
|
|
14742
|
+
existing.statusRank = Math.min(existing.statusRank, rank);
|
|
14743
|
+
};
|
|
14744
|
+
const health = bundle?.diagnosis?.health;
|
|
14745
|
+
const fromDiag = opts.vitals ?? health?.vital_signs ?? [];
|
|
14746
|
+
const gating = opts.prefer?.[0] ?? health?.gating_vital_sign;
|
|
14747
|
+
if (gating) upsert(String(gating), 0, "red");
|
|
14748
|
+
for (const vs of fromDiag) {
|
|
14749
|
+
const id = vs.vital_sign;
|
|
14750
|
+
upsert(id, prefer.has(id) ? 0 : 1, vs.status);
|
|
14751
|
+
}
|
|
14752
|
+
const rawMetrics = opts.metrics ?? bundle?.metrics?.metrics ?? [];
|
|
14753
|
+
let sawMetrics = rawMetrics.length > 0;
|
|
14754
|
+
for (const row of rawMetrics) {
|
|
14755
|
+
const id = String(row.metric ?? row.metric ?? "");
|
|
14756
|
+
if (!id) continue;
|
|
14757
|
+
const status = String(row.status ?? row.status ?? "");
|
|
14758
|
+
const value = row.value ?? row.value;
|
|
14759
|
+
const unavailable = row.unavailable_reason ?? row.unavailable_reason;
|
|
14760
|
+
if (value == null && unavailable) continue;
|
|
14761
|
+
upsert(id, prefer.has(id) ? 0 : 2, status);
|
|
14762
|
+
}
|
|
14763
|
+
if (sawMetrics || bundle?.metrics) {
|
|
14764
|
+
for (const id of ["arr", "nrr", "pipeline_coverage", "win_rate"]) {
|
|
14765
|
+
if (!map.has(id) && getMetricExplainer(id)) {
|
|
14766
|
+
upsert(id, 3, "neutral");
|
|
14767
|
+
}
|
|
13593
14768
|
}
|
|
13594
|
-
lines.push("");
|
|
13595
14769
|
}
|
|
13596
|
-
if (
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
for (const key of KEY_METRICS) {
|
|
13600
|
-
const row = byKey.get(key);
|
|
13601
|
-
if (row) lines.push(formatMetricLine(row));
|
|
13602
|
-
}
|
|
13603
|
-
if (metrics.findings.length > 0) {
|
|
13604
|
-
lines.push("");
|
|
13605
|
-
lines.push("### Metrics findings");
|
|
13606
|
-
appendFindings(lines, metrics.findings);
|
|
14770
|
+
if (map.size === 0) {
|
|
14771
|
+
for (const id of ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"]) {
|
|
14772
|
+
upsert(id, 4, "neutral");
|
|
13607
14773
|
}
|
|
13608
|
-
lines.push("");
|
|
13609
|
-
}
|
|
13610
|
-
if (!diagnosis && metrics) {
|
|
13611
|
-
lines.unshift("Primary analysis: SaaS metrics (no GTM health snapshot on this session yet).", "");
|
|
13612
|
-
} else if (diagnosis && !metrics) {
|
|
13613
|
-
lines.push("(SaaS metrics not run on this session \u2014 run /metrics for the revenue view)");
|
|
13614
14774
|
}
|
|
13615
|
-
return
|
|
14775
|
+
return [...map.values()].sort((a, b) => {
|
|
14776
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
14777
|
+
if (a.statusRank !== b.statusRank) return a.statusRank - b.statusRank;
|
|
14778
|
+
return a.id.localeCompare(b.id);
|
|
14779
|
+
});
|
|
13616
14780
|
}
|
|
13617
|
-
function
|
|
13618
|
-
const
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
inFindings = true;
|
|
13630
|
-
out.push(line);
|
|
13631
|
-
continue;
|
|
13632
|
-
}
|
|
13633
|
-
if (inFindings && line.startsWith("- [")) {
|
|
13634
|
-
if (findingCount >= 5) continue;
|
|
13635
|
-
out.push(line);
|
|
13636
|
-
findingCount++;
|
|
13637
|
-
continue;
|
|
13638
|
-
}
|
|
13639
|
-
if (inFindings && line.startsWith("##")) {
|
|
13640
|
-
inFindings = false;
|
|
13641
|
-
}
|
|
13642
|
-
if (line.startsWith("## ") || line.startsWith("### Vital") || line.startsWith("- ") && !inFindings) {
|
|
13643
|
-
if (line.startsWith("(SaaS metrics not run")) continue;
|
|
13644
|
-
out.push(line);
|
|
13645
|
-
}
|
|
13646
|
-
if (line.startsWith("Overall score:") || line.startsWith("Total value at risk:")) {
|
|
13647
|
-
out.push(line);
|
|
13648
|
-
}
|
|
13649
|
-
if (line.startsWith("- ARR:") || line.startsWith("- NRR:") || line.startsWith("- GRR:")) {
|
|
13650
|
-
out.push(line);
|
|
14781
|
+
function formatEntry(explainer, audience) {
|
|
14782
|
+
const framing = audience === "ops" ? explainer.audience.ops : explainer.audience.board;
|
|
14783
|
+
const lines = [];
|
|
14784
|
+
lines.push(`### ${explainer.label} (\`${explainer.id}\`)`);
|
|
14785
|
+
lines.push("");
|
|
14786
|
+
lines.push(framing);
|
|
14787
|
+
lines.push("");
|
|
14788
|
+
if (audience === "ops") {
|
|
14789
|
+
lines.push("**How it's calculated**");
|
|
14790
|
+
lines.push("");
|
|
14791
|
+
for (const f of explainer.formula_lines) {
|
|
14792
|
+
lines.push(`- \`${f}\``);
|
|
13651
14793
|
}
|
|
14794
|
+
lines.push("");
|
|
14795
|
+
} else {
|
|
14796
|
+
lines.push(`*${explainer.tagline}*`);
|
|
14797
|
+
lines.push("");
|
|
13652
14798
|
}
|
|
13653
|
-
return
|
|
13654
|
-
}
|
|
13655
|
-
function appendFindings(lines, findings) {
|
|
13656
|
-
for (const f of findings.slice(0, 12)) {
|
|
13657
|
-
const dollars = f.dollar_value != null ? ` (${formatCurrency(f.dollar_value)})` : "";
|
|
13658
|
-
const plays = f.recommended_plays?.length ? ` \u2192 Plays: ${f.recommended_plays.map((p) => p.play_name).join(", ")}` : "";
|
|
13659
|
-
lines.push(`- [${f.severity}]${dollars} ${f.finding}${plays}`);
|
|
13660
|
-
}
|
|
14799
|
+
return lines;
|
|
13661
14800
|
}
|
|
13662
|
-
function
|
|
13663
|
-
|
|
13664
|
-
|
|
14801
|
+
function buildDefinitionsAppendix(bundle, opts = {}) {
|
|
14802
|
+
const audience = normalizeAudience(opts.audience);
|
|
14803
|
+
const cap = opts.cap ?? DEFAULT_CAP;
|
|
14804
|
+
const candidates = collectCandidates(bundle, opts).slice(0, cap);
|
|
14805
|
+
if (candidates.length === 0) return "";
|
|
14806
|
+
const lines = [];
|
|
14807
|
+
lines.push(`## Metric definitions (for the ${audienceLabel(audience)})`);
|
|
14808
|
+
lines.push("");
|
|
14809
|
+
lines.push(
|
|
14810
|
+
audience === "ops" ? "Formula-first brief for operators executing the plan. Full slides: `/deepdive <metric>`." : "Meaning-first brief for the room. Full slides: `/deepdive <metric>`."
|
|
14811
|
+
);
|
|
14812
|
+
lines.push("");
|
|
14813
|
+
for (const c of candidates) {
|
|
14814
|
+
const explainer = getMetricExplainer(c.id);
|
|
14815
|
+
if (!explainer) continue;
|
|
14816
|
+
lines.push(...formatEntry(explainer, audience));
|
|
13665
14817
|
}
|
|
13666
|
-
return "
|
|
14818
|
+
return lines.join("\n");
|
|
13667
14819
|
}
|
|
13668
|
-
var
|
|
13669
|
-
var
|
|
13670
|
-
"src/services/
|
|
14820
|
+
var DEFAULT_CAP;
|
|
14821
|
+
var init_metric_explainers = __esm({
|
|
14822
|
+
"src/services/metric-explainers.ts"() {
|
|
13671
14823
|
"use strict";
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
init_formatters();
|
|
13675
|
-
init_theme();
|
|
13676
|
-
KEY_METRICS = ["arr", "nrr", "grr", "win_rate", "pipeline_coverage"];
|
|
14824
|
+
init_metric_definitions();
|
|
14825
|
+
DEFAULT_CAP = 8;
|
|
13677
14826
|
}
|
|
13678
14827
|
});
|
|
13679
14828
|
|
|
@@ -13715,16 +14864,24 @@ function buildOpenQuestions(ctx) {
|
|
|
13715
14864
|
}
|
|
13716
14865
|
return lines.length > 0 ? lines.join("\n") : "(No open questions recorded.)";
|
|
13717
14866
|
}
|
|
13718
|
-
function
|
|
14867
|
+
function audiencePhrase(audience) {
|
|
14868
|
+
if (!audience) return "an executive audience";
|
|
14869
|
+
const a = audience.toLowerCase();
|
|
14870
|
+
if (a === "ops" || a === "operations") return "an ops / operator audience";
|
|
14871
|
+
if (a === "board") return "a board / exec audience";
|
|
14872
|
+
return `a ${audience} audience`;
|
|
14873
|
+
}
|
|
14874
|
+
function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions, definitionsBlock, ctx) {
|
|
13719
14875
|
const company = loadProfile()?.company_name ?? "the company";
|
|
13720
14876
|
const contextLabel = handoffInstructionPrefix(ctx.analysis.primary);
|
|
14877
|
+
const forWhom = audiencePhrase(ctx.scope?.audience);
|
|
13721
14878
|
const instructions = {
|
|
13722
|
-
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.`,
|
|
13723
|
-
asana: `produce an Asana project plan with sections and tasks tied to findings. Prioritize by dollar impact.`,
|
|
13724
|
-
clay: `produce a Clay table specification to operationalize the highest-impact finding.`,
|
|
13725
|
-
plan: `produce a prioritized action plan with problem, play, first 3 steps, owner, and leading indicator per item.`
|
|
14879
|
+
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.`,
|
|
14880
|
+
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.`,
|
|
14881
|
+
clay: `produce a Clay table specification to operationalize the highest-impact finding for ${forWhom}.`,
|
|
14882
|
+
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.`
|
|
13726
14883
|
};
|
|
13727
|
-
|
|
14884
|
+
const parts = [
|
|
13728
14885
|
`# NTRP handoff \u2192 ${target}`,
|
|
13729
14886
|
"",
|
|
13730
14887
|
`You are an expert GTM operator. Using ${contextLabel}, ${instructions[target]}`,
|
|
@@ -13740,25 +14897,35 @@ function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions,
|
|
|
13740
14897
|
conversationBlock,
|
|
13741
14898
|
"",
|
|
13742
14899
|
"---",
|
|
13743
|
-
"",
|
|
13744
|
-
"## Open questions",
|
|
13745
|
-
"",
|
|
13746
|
-
openQuestions,
|
|
13747
|
-
"",
|
|
13748
|
-
"---",
|
|
13749
14900
|
""
|
|
13750
|
-
]
|
|
14901
|
+
];
|
|
14902
|
+
if (definitionsBlock.trim()) {
|
|
14903
|
+
parts.push(definitionsBlock.trim(), "", "---", "");
|
|
14904
|
+
}
|
|
14905
|
+
parts.push("## Open questions", "", openQuestions, "", "---", "");
|
|
14906
|
+
return parts.join("\n");
|
|
13751
14907
|
}
|
|
13752
14908
|
async function buildDeliverableDraft(ctx, target = "plan") {
|
|
13753
14909
|
const bundle = await loadSessionAnalysisBundle();
|
|
13754
14910
|
const analysis = buildHandoffContextBlock(bundle, ctx);
|
|
13755
14911
|
const conversation = buildConversationSection(ctx);
|
|
13756
14912
|
const open_questions = buildOpenQuestions(ctx);
|
|
14913
|
+
const definitions = buildDefinitionsAppendix(bundle, {
|
|
14914
|
+
audience: ctx.scope?.audience,
|
|
14915
|
+
prefer: bundle.diagnosis?.health.gating_vital_sign ? [bundle.diagnosis.health.gating_vital_sign] : void 0
|
|
14916
|
+
});
|
|
13757
14917
|
if (!analysis && ctx.messages.length === 0) return null;
|
|
13758
|
-
const markdown = wrapForTarget(
|
|
14918
|
+
const markdown = wrapForTarget(
|
|
14919
|
+
target,
|
|
14920
|
+
analysis,
|
|
14921
|
+
conversation,
|
|
14922
|
+
open_questions,
|
|
14923
|
+
definitions,
|
|
14924
|
+
ctx
|
|
14925
|
+
);
|
|
13759
14926
|
return {
|
|
13760
14927
|
markdown,
|
|
13761
|
-
sections: { analysis, conversation, open_questions }
|
|
14928
|
+
sections: { analysis, conversation, open_questions, definitions }
|
|
13762
14929
|
};
|
|
13763
14930
|
}
|
|
13764
14931
|
function inferHandoffTarget(input) {
|
|
@@ -13778,6 +14945,7 @@ var init_handoff_draft = __esm({
|
|
|
13778
14945
|
"use strict";
|
|
13779
14946
|
init_profile();
|
|
13780
14947
|
init_session_analysis();
|
|
14948
|
+
init_metric_explainers();
|
|
13781
14949
|
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;
|
|
13782
14950
|
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;
|
|
13783
14951
|
}
|
|
@@ -15003,94 +16171,94 @@ var init_strategist2 = __esm({
|
|
|
15003
16171
|
});
|
|
15004
16172
|
|
|
15005
16173
|
// src/output/strategy-brief.ts
|
|
15006
|
-
import
|
|
16174
|
+
import chalk13 from "chalk";
|
|
15007
16175
|
function printWrapped(text, width, prefix = INDENT, style) {
|
|
15008
16176
|
for (const line of wrapWords(text, width)) {
|
|
15009
16177
|
console.log(prefix + (style ? style(line) : line));
|
|
15010
16178
|
}
|
|
15011
16179
|
}
|
|
15012
16180
|
function outcomeLine(outcome) {
|
|
15013
|
-
return `${
|
|
16181
|
+
return `${chalk13.bold(outcome.metric)}: ${outcome.baseline} ${chalk13.dim("->")} ${chalk13.bold(outcome.target_range)} ${chalk13.dim(`by ${outcome.check_date} \xB7 ${outcome.measured_by}`)}`;
|
|
15014
16182
|
}
|
|
15015
16183
|
function printWorkstream(ws, width) {
|
|
15016
|
-
const plays = ws.play_ids.length > 0 ?
|
|
15017
|
-
console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${
|
|
15018
|
-
printWrapped(ws.problem, width - 5, INDENT + " ", (s) =>
|
|
16184
|
+
const plays = ws.play_ids.length > 0 ? chalk13.dim(` play: ${ws.play_ids.join(", ")}`) : "";
|
|
16185
|
+
console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${chalk13.bold(ws.title)}${plays}`);
|
|
16186
|
+
printWrapped(ws.problem, width - 5, INDENT + " ", (s) => chalk13.dim(s));
|
|
15019
16187
|
if (ws.rationale) {
|
|
15020
|
-
printWrapped(`Why now: ${ws.rationale}`, width - 5, INDENT + " ", (s) =>
|
|
16188
|
+
printWrapped(`Why now: ${ws.rationale}`, width - 5, INDENT + " ", (s) => chalk13.dim(s));
|
|
15021
16189
|
}
|
|
15022
16190
|
console.log(`${INDENT} ${outcomeLine(ws.expected_outcome)}`);
|
|
15023
16191
|
for (const li of ws.leading_indicators) {
|
|
15024
|
-
console.log(`${INDENT} ${
|
|
16192
|
+
console.log(`${INDENT} ${chalk13.dim("leads:")} ${outcomeLine(li)}`);
|
|
15025
16193
|
}
|
|
15026
16194
|
if (ws.milestones.length > 0) {
|
|
15027
|
-
console.log(`${INDENT} ${
|
|
16195
|
+
console.log(`${INDENT} ${chalk13.dim("Milestones")}`);
|
|
15028
16196
|
for (const m of ws.milestones) {
|
|
15029
|
-
console.log(`${INDENT} ${paint("accent", m.due)} ${m.label} ${
|
|
16197
|
+
console.log(`${INDENT} ${paint("accent", m.due)} ${m.label} ${chalk13.dim(`(verify: ${m.verification})`)}`);
|
|
15030
16198
|
}
|
|
15031
16199
|
}
|
|
15032
16200
|
if (ws.deliverables.length > 0) {
|
|
15033
|
-
console.log(`${INDENT} ${
|
|
16201
|
+
console.log(`${INDENT} ${chalk13.dim("Deliverables")}`);
|
|
15034
16202
|
for (const d of ws.deliverables) {
|
|
15035
|
-
console.log(`${INDENT} ${
|
|
16203
|
+
console.log(`${INDENT} ${chalk13.dim("[ ]")} ${d.label} ${chalk13.dim(`(${d.kind.replace("_", " ")} \xB7 due ${d.due})`)}`);
|
|
15036
16204
|
}
|
|
15037
16205
|
}
|
|
15038
16206
|
if (ws.actions.length > 0) {
|
|
15039
|
-
console.log(`${INDENT} ${
|
|
16207
|
+
console.log(`${INDENT} ${chalk13.dim("First actions")}`);
|
|
15040
16208
|
for (const action of ws.actions.slice(0, 4)) {
|
|
15041
|
-
printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) =>
|
|
16209
|
+
printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) => chalk13.dim(s));
|
|
15042
16210
|
}
|
|
15043
16211
|
}
|
|
15044
16212
|
printWrapped(
|
|
15045
16213
|
`If ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) -> ${ws.contingency.fallback}`,
|
|
15046
16214
|
width - 5,
|
|
15047
16215
|
INDENT + " ",
|
|
15048
|
-
(s) =>
|
|
16216
|
+
(s) => chalk13.hex("#eab308")(s)
|
|
15049
16217
|
);
|
|
15050
|
-
console.log(`${INDENT} ${
|
|
16218
|
+
console.log(`${INDENT} ${chalk13.dim(`~${Math.round(ws.effort_hours)} team-hours`)}`);
|
|
15051
16219
|
console.log();
|
|
15052
16220
|
}
|
|
15053
16221
|
function printStrategyBrief(plan, stats) {
|
|
15054
16222
|
const width = Math.min(termWidth() - 4, 92);
|
|
15055
16223
|
console.log();
|
|
15056
16224
|
console.log(
|
|
15057
|
-
`${INDENT}${
|
|
16225
|
+
`${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()}`)}`
|
|
15058
16226
|
);
|
|
15059
|
-
console.log(INDENT +
|
|
16227
|
+
console.log(INDENT + chalk13.dim(hr(width)));
|
|
15060
16228
|
printWrapped(`Objective: ${plan.objective}`, width, INDENT, (s) => paint("accent", s));
|
|
15061
16229
|
console.log();
|
|
15062
|
-
console.log(`${INDENT}${
|
|
16230
|
+
console.log(`${INDENT}${chalk13.dim("30,000 ft")}`);
|
|
15063
16231
|
printWrapped(plan.summary_30k, width);
|
|
15064
16232
|
console.log();
|
|
15065
16233
|
for (const ws of plan.workstreams) {
|
|
15066
16234
|
printWorkstream(ws, width);
|
|
15067
16235
|
}
|
|
15068
16236
|
if (plan.constraints.length > 0) {
|
|
15069
|
-
console.log(`${INDENT}${
|
|
16237
|
+
console.log(`${INDENT}${chalk13.dim("Constraints")}`);
|
|
15070
16238
|
for (const c of plan.constraints) {
|
|
15071
|
-
printWrapped(`- ${c}`, width - 2, INDENT, (s) =>
|
|
16239
|
+
printWrapped(`- ${c}`, width - 2, INDENT, (s) => chalk13.dim(s));
|
|
15072
16240
|
}
|
|
15073
16241
|
console.log();
|
|
15074
16242
|
}
|
|
15075
16243
|
if (plan.assumptions.length > 0) {
|
|
15076
|
-
console.log(`${INDENT}${
|
|
16244
|
+
console.log(`${INDENT}${chalk13.dim("Assumptions (unverified \u2014 not counted as targets)")}`);
|
|
15077
16245
|
for (const a of plan.assumptions) {
|
|
15078
|
-
printWrapped(`- ${a}`, width - 2, INDENT, (s) =>
|
|
16246
|
+
printWrapped(`- ${a}`, width - 2, INDENT, (s) => chalk13.dim(s));
|
|
15079
16247
|
}
|
|
15080
16248
|
console.log();
|
|
15081
16249
|
}
|
|
15082
16250
|
if (plan.risks.length > 0) {
|
|
15083
|
-
console.log(`${INDENT}${
|
|
16251
|
+
console.log(`${INDENT}${chalk13.dim("Risks")}`);
|
|
15084
16252
|
for (const r of plan.risks) {
|
|
15085
|
-
printWrapped(`- ${r}`, width - 2, INDENT, (s) =>
|
|
16253
|
+
printWrapped(`- ${r}`, width - 2, INDENT, (s) => chalk13.dim(s));
|
|
15086
16254
|
}
|
|
15087
16255
|
console.log();
|
|
15088
16256
|
}
|
|
15089
16257
|
const totalHours = plan.workstreams.reduce((sum, ws) => sum + ws.effort_hours, 0);
|
|
15090
|
-
console.log(INDENT +
|
|
16258
|
+
console.log(INDENT + chalk13.dim(hr(width)));
|
|
15091
16259
|
const coverage = stats.total_targets > 0 ? `${stats.measurable_targets} of ${stats.total_targets} targets measurable with current data` : "no quantified targets";
|
|
15092
|
-
const coverageStyled = stats.total_targets > 0 && stats.measurable_targets === stats.total_targets ? paint("success", coverage) :
|
|
15093
|
-
console.log(`${INDENT}${coverageStyled}${
|
|
16260
|
+
const coverageStyled = stats.total_targets > 0 && stats.measurable_targets === stats.total_targets ? paint("success", coverage) : chalk13.hex("#eab308")(coverage);
|
|
16261
|
+
console.log(`${INDENT}${coverageStyled}${chalk13.dim(` \xB7 ~${Math.round(totalHours)} total team-hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
|
|
15094
16262
|
console.log();
|
|
15095
16263
|
}
|
|
15096
16264
|
var INDENT;
|
|
@@ -15115,7 +16283,7 @@ __export(strategist_flow_exports, {
|
|
|
15115
16283
|
resumeStrategistAfterConnect: () => resumeStrategistAfterConnect,
|
|
15116
16284
|
startStrategistFlow: () => startStrategistFlow
|
|
15117
16285
|
});
|
|
15118
|
-
import
|
|
16286
|
+
import chalk14 from "chalk";
|
|
15119
16287
|
function isStrategistIntent(input) {
|
|
15120
16288
|
const line = input.trim();
|
|
15121
16289
|
if (!line) return false;
|
|
@@ -15135,11 +16303,11 @@ function queueStrategistForAnalysis(ctx, opts) {
|
|
|
15135
16303
|
saveSessionState(ctx);
|
|
15136
16304
|
console.log();
|
|
15137
16305
|
console.log(
|
|
15138
|
-
" " +
|
|
16306
|
+
" " + chalk14.dim("Strategy session queued \u2014 I'll build the plan once your data is analyzed.")
|
|
15139
16307
|
);
|
|
15140
16308
|
if (opts.origin !== "nl") {
|
|
15141
16309
|
console.log(
|
|
15142
|
-
" " +
|
|
16310
|
+
" " + chalk14.dim("Tell me what to look at, paste a CSV path, or say ") + chalk14.cyan("use demo data") + chalk14.dim(".")
|
|
15143
16311
|
);
|
|
15144
16312
|
console.log();
|
|
15145
16313
|
}
|
|
@@ -15158,7 +16326,7 @@ async function startStrategistFlow(ctx, opts) {
|
|
|
15158
16326
|
ctx.strategistState = { step: "objective_input", origin: opts.origin };
|
|
15159
16327
|
saveSessionState(ctx);
|
|
15160
16328
|
console.log();
|
|
15161
|
-
console.log(" " +
|
|
16329
|
+
console.log(" " + chalk14.dim(`What's the objective? State it like a finish line \u2014 e.g. "cut stale pipeline in half before Q4".`));
|
|
15162
16330
|
console.log();
|
|
15163
16331
|
recordMessage(ctx, "agent", "Strategist: asked for objective");
|
|
15164
16332
|
return "Awaiting objective";
|
|
@@ -15195,14 +16363,14 @@ async function handleStrategizeFlow(input, ctx) {
|
|
|
15195
16363
|
ctx.strategistState = void 0;
|
|
15196
16364
|
saveSessionState(ctx);
|
|
15197
16365
|
console.log();
|
|
15198
|
-
console.log(" " +
|
|
16366
|
+
console.log(" " + chalk14.dim("Strategy session cancelled \u2014 back to exploring."));
|
|
15199
16367
|
console.log();
|
|
15200
16368
|
return "Strategy cancelled";
|
|
15201
16369
|
}
|
|
15202
16370
|
if (state2.step === "objective_input") {
|
|
15203
16371
|
if (line.length < 8) {
|
|
15204
16372
|
console.log();
|
|
15205
|
-
console.log(" " +
|
|
16373
|
+
console.log(" " + chalk14.dim("Give me a bit more \u2014 what outcome are we planning toward?"));
|
|
15206
16374
|
console.log();
|
|
15207
16375
|
return "Awaiting objective";
|
|
15208
16376
|
}
|
|
@@ -15219,17 +16387,17 @@ async function handleStrategizeFlow(input, ctx) {
|
|
|
15219
16387
|
state2.step = "objective_input";
|
|
15220
16388
|
saveSessionState(ctx);
|
|
15221
16389
|
console.log();
|
|
15222
|
-
console.log(" " +
|
|
16390
|
+
console.log(" " + chalk14.dim("What's the objective? State it like a finish line."));
|
|
15223
16391
|
console.log();
|
|
15224
16392
|
return "Awaiting objective";
|
|
15225
16393
|
}
|
|
15226
16394
|
if (QUESTION_RE.test(line)) {
|
|
15227
16395
|
console.log();
|
|
15228
16396
|
console.log(
|
|
15229
|
-
" " +
|
|
16397
|
+
" " + chalk14.dim("That looks like a question \u2014 I'm holding a strategy objective right now.")
|
|
15230
16398
|
);
|
|
15231
16399
|
console.log(
|
|
15232
|
-
" " +
|
|
16400
|
+
" " + 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.")
|
|
15233
16401
|
);
|
|
15234
16402
|
console.log();
|
|
15235
16403
|
return "Awaiting confirm";
|
|
@@ -15242,7 +16410,7 @@ async function handleStrategizeFlow(input, ctx) {
|
|
|
15242
16410
|
}
|
|
15243
16411
|
console.log();
|
|
15244
16412
|
console.log(
|
|
15245
|
-
" " +
|
|
16413
|
+
" " + 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(".")
|
|
15246
16414
|
);
|
|
15247
16415
|
console.log();
|
|
15248
16416
|
return "Awaiting confirm";
|
|
@@ -15305,7 +16473,7 @@ async function runStrategistSession(ctx) {
|
|
|
15305
16473
|
break;
|
|
15306
16474
|
case "thinking":
|
|
15307
16475
|
spinner.stop();
|
|
15308
|
-
console.log(" " +
|
|
16476
|
+
console.log(" " + chalk14.dim.italic(event.text));
|
|
15309
16477
|
spinner.start();
|
|
15310
16478
|
break;
|
|
15311
16479
|
case "notice":
|
|
@@ -15327,17 +16495,17 @@ async function runStrategistSession(ctx) {
|
|
|
15327
16495
|
spinner.stop();
|
|
15328
16496
|
} catch (err) {
|
|
15329
16497
|
spinner.fail("Strategy session failed");
|
|
15330
|
-
console.error(" " +
|
|
16498
|
+
console.error(" " + chalk14.red(String(err.message ?? err)));
|
|
15331
16499
|
ctx.strategistState = void 0;
|
|
15332
16500
|
saveSessionState(ctx);
|
|
15333
16501
|
console.log(
|
|
15334
|
-
" " +
|
|
16502
|
+
" " + chalk14.dim('Strategy session dropped \u2014 say "how should we fix this?" or run ') + paint("accent", "/strategy") + chalk14.dim(" to retry.")
|
|
15335
16503
|
);
|
|
15336
16504
|
console.log();
|
|
15337
16505
|
return;
|
|
15338
16506
|
}
|
|
15339
16507
|
if (!plan) {
|
|
15340
|
-
console.log(" " +
|
|
16508
|
+
console.log(" " + chalk14.dim("(no plan produced)"));
|
|
15341
16509
|
ctx.strategistState = void 0;
|
|
15342
16510
|
saveSessionState(ctx);
|
|
15343
16511
|
console.log();
|
|
@@ -15345,7 +16513,7 @@ async function runStrategistSession(ctx) {
|
|
|
15345
16513
|
}
|
|
15346
16514
|
printStrategyBrief(plan, stats);
|
|
15347
16515
|
for (const notice of notices.slice(0, 6)) {
|
|
15348
|
-
console.log(" " +
|
|
16516
|
+
console.log(" " + chalk14.dim(notice));
|
|
15349
16517
|
}
|
|
15350
16518
|
printLlmAttribution(meta);
|
|
15351
16519
|
console.log();
|
|
@@ -15370,18 +16538,18 @@ async function runStrategistSession(ctx) {
|
|
|
15370
16538
|
creditStrategySession(ctx);
|
|
15371
16539
|
console.log();
|
|
15372
16540
|
console.log(" " + paint("accent", `Strategy saved: ${persisted.strategy.title}`));
|
|
15373
|
-
console.log(" " +
|
|
16541
|
+
console.log(" " + chalk14.dim(persisted.library_path));
|
|
15374
16542
|
console.log(
|
|
15375
|
-
" " +
|
|
16543
|
+
" " + chalk14.dim("Check progress anytime with ") + paint("accent", `/strategy review ${persisted.strategy.slug}`) + chalk14.dim(" \u2014 future answers will reference this plan.")
|
|
15376
16544
|
);
|
|
15377
16545
|
console.log();
|
|
15378
16546
|
recordMessage(ctx, "agent", `Strategy saved: ${persisted.strategy.title} (${persisted.strategy.slug})`);
|
|
15379
16547
|
} catch (err) {
|
|
15380
|
-
console.error(" " +
|
|
16548
|
+
console.error(" " + chalk14.red(`Could not save strategy: ${String(err.message ?? err)}`));
|
|
15381
16549
|
console.log();
|
|
15382
16550
|
}
|
|
15383
16551
|
} else {
|
|
15384
|
-
console.log(" " +
|
|
16552
|
+
console.log(" " + chalk14.dim("Kept as a working draft \u2014 not saved to the library."));
|
|
15385
16553
|
console.log();
|
|
15386
16554
|
recordMessage(ctx, "agent", `Strategy drafted (unsaved): ${plan.title}`);
|
|
15387
16555
|
}
|
|
@@ -15410,16 +16578,16 @@ async function ensureSnapshot(ctx) {
|
|
|
15410
16578
|
}
|
|
15411
16579
|
function printObjectiveCard(ctx, objective, proposed) {
|
|
15412
16580
|
console.log();
|
|
15413
|
-
console.log(" " +
|
|
16581
|
+
console.log(" " + chalk14.bold("Strategy session"));
|
|
15414
16582
|
console.log(
|
|
15415
|
-
" " +
|
|
16583
|
+
" " + chalk14.dim(proposed ? "Proposed objective: " : "Objective: ") + paint("accent", objective)
|
|
15416
16584
|
);
|
|
15417
16585
|
console.log(
|
|
15418
|
-
" " +
|
|
16586
|
+
" " + chalk14.dim("I'll ground it in your live data, sequence the fixes, set measurable milestones, and stress-test the plan.")
|
|
15419
16587
|
);
|
|
15420
16588
|
console.log();
|
|
15421
16589
|
console.log(
|
|
15422
|
-
" " +
|
|
16590
|
+
" " + chalk14.dim("Confirm? ") + chalk14.cyan("\u23CE yes") + chalk14.dim(" \xB7 ") + chalk14.cyan("adjust") + chalk14.dim(" \xB7 ") + chalk14.cyan("cancel")
|
|
15423
16591
|
);
|
|
15424
16592
|
console.log();
|
|
15425
16593
|
}
|
|
@@ -15440,35 +16608,35 @@ async function printKeylessSkeletonPlan(ctx, objective) {
|
|
|
15440
16608
|
LAYERS2
|
|
15441
16609
|
);
|
|
15442
16610
|
if (triggered.length > 0) {
|
|
15443
|
-
console.log(" " +
|
|
15444
|
-
console.log(" " +
|
|
15445
|
-
console.log(" " +
|
|
16611
|
+
console.log(" " + chalk14.bold("Skeleton plan") + chalk14.dim(" \u2014 deterministic, from your computed vitals (no AI)"));
|
|
16612
|
+
console.log(" " + chalk14.dim(`Objective: ${objective}`));
|
|
16613
|
+
console.log(" " + chalk14.dim("Ordered by dependency: clean data gates moving pipeline gates efficient effort."));
|
|
15446
16614
|
console.log();
|
|
15447
16615
|
triggered.forEach(({ play, vital }, index) => {
|
|
15448
16616
|
const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? ` \xB7 ${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? ""}`.trimEnd() : "";
|
|
15449
16617
|
console.log(
|
|
15450
|
-
` ${paint("accent", `${index + 1}.`)} ${
|
|
16618
|
+
` ${paint("accent", `${index + 1}.`)} ${chalk14.bold(play.name)} ${chalk14.dim(`(${play.id})`)}`
|
|
15451
16619
|
);
|
|
15452
16620
|
console.log(
|
|
15453
|
-
" " +
|
|
16621
|
+
" " + chalk14.dim(`${vital.vital_sign} ${Math.round(vital.score)} (${vital.status})${dollar}`)
|
|
15454
16622
|
);
|
|
15455
|
-
console.log(" " +
|
|
16623
|
+
console.log(" " + chalk14.dim(`Why: ${play.why.split(". ")[0]}.`));
|
|
15456
16624
|
if (play.steps[0]) {
|
|
15457
|
-
console.log(" " +
|
|
16625
|
+
console.log(" " + chalk14.dim(`First step: ${play.steps[0]}`));
|
|
15458
16626
|
}
|
|
15459
|
-
console.log(" " +
|
|
16627
|
+
console.log(" " + chalk14.dim(`Expected: ${play.expected_outcome}`));
|
|
15460
16628
|
console.log();
|
|
15461
16629
|
});
|
|
15462
16630
|
} else {
|
|
15463
|
-
console.log(" " +
|
|
16631
|
+
console.log(" " + chalk14.bold("No plays triggered") + chalk14.dim(" \u2014 every vital sign is above its play threshold."));
|
|
15464
16632
|
console.log();
|
|
15465
16633
|
}
|
|
15466
16634
|
}
|
|
15467
16635
|
console.log(
|
|
15468
|
-
" " +
|
|
16636
|
+
" " + 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.")
|
|
15469
16637
|
);
|
|
15470
16638
|
console.log(
|
|
15471
|
-
" " +
|
|
16639
|
+
" " + 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.")
|
|
15472
16640
|
);
|
|
15473
16641
|
console.log();
|
|
15474
16642
|
}
|
|
@@ -15514,7 +16682,7 @@ __export(keyless_ask_exports, {
|
|
|
15514
16682
|
isKeylessVitalsAsk: () => isKeylessVitalsAsk,
|
|
15515
16683
|
tryKeylessAskAnswer: () => tryKeylessAskAnswer
|
|
15516
16684
|
});
|
|
15517
|
-
import
|
|
16685
|
+
import chalk15 from "chalk";
|
|
15518
16686
|
function isKeylessVitalsAsk(input) {
|
|
15519
16687
|
return KEYLESS_ASK_RE.test(input.trim());
|
|
15520
16688
|
}
|
|
@@ -15563,35 +16731,35 @@ async function tryKeylessAskAnswer(ctx, input, opts = {}) {
|
|
|
15563
16731
|
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.`;
|
|
15564
16732
|
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);
|
|
15565
16733
|
console.log();
|
|
15566
|
-
console.log(" " +
|
|
16734
|
+
console.log(" " + chalk15.bold(headline));
|
|
15567
16735
|
if (opts.fromResume) {
|
|
15568
16736
|
if (runners.length > 0) {
|
|
15569
16737
|
console.log(
|
|
15570
|
-
" " +
|
|
16738
|
+
" " + chalk15.dim("Next after that: ") + chalk15.dim(runners.map(formatRunnerBit).join(" \xB7 "))
|
|
15571
16739
|
);
|
|
15572
16740
|
}
|
|
15573
16741
|
} else {
|
|
15574
16742
|
console.log();
|
|
15575
16743
|
if (gating && gating.vital_sign !== primary.vital_sign) {
|
|
15576
16744
|
console.log(
|
|
15577
|
-
" " +
|
|
16745
|
+
" " + 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.`)
|
|
15578
16746
|
);
|
|
15579
16747
|
}
|
|
15580
16748
|
if (runners.length > 0) {
|
|
15581
|
-
console.log(" " +
|
|
16749
|
+
console.log(" " + chalk15.dim("Also on the board:"));
|
|
15582
16750
|
for (const vs of runners) {
|
|
15583
|
-
console.log(" " +
|
|
16751
|
+
console.log(" " + chalk15.dim("\xB7 ") + formatVitalLine(vs));
|
|
15584
16752
|
}
|
|
15585
16753
|
}
|
|
15586
16754
|
if (aggregate.total_value_at_risk != null && aggregate.total_value_at_risk > 0) {
|
|
15587
16755
|
console.log(
|
|
15588
|
-
" " +
|
|
16756
|
+
" " + chalk15.dim("Total at risk: ") + chalk15.green(formatCurrency(aggregate.total_value_at_risk))
|
|
15589
16757
|
);
|
|
15590
16758
|
}
|
|
15591
16759
|
}
|
|
15592
16760
|
console.log();
|
|
15593
16761
|
console.log(
|
|
15594
|
-
" " +
|
|
16762
|
+
" " + 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.")
|
|
15595
16763
|
);
|
|
15596
16764
|
console.log();
|
|
15597
16765
|
if (!opts.fromResume) {
|
|
@@ -15613,11 +16781,119 @@ var init_keyless_ask = __esm({
|
|
|
15613
16781
|
}
|
|
15614
16782
|
});
|
|
15615
16783
|
|
|
16784
|
+
// src/conversation/keyless-definitions.ts
|
|
16785
|
+
import chalk16 from "chalk";
|
|
16786
|
+
function isPossessiveMetricAsk(input) {
|
|
16787
|
+
return POSSESSIVE_RE.test(input.trim());
|
|
16788
|
+
}
|
|
16789
|
+
function isDefinitionAsk(input) {
|
|
16790
|
+
const line = input.trim();
|
|
16791
|
+
if (!line) return false;
|
|
16792
|
+
if (isPossessiveMetricAsk(line)) return false;
|
|
16793
|
+
return DEFINITION_RE.test(line) || MEAN_RE.test(line);
|
|
16794
|
+
}
|
|
16795
|
+
function extractDefinitionQuery(input) {
|
|
16796
|
+
const line = input.trim().replace(/[?.!]+$/, "");
|
|
16797
|
+
const mean = line.match(MEAN_RE);
|
|
16798
|
+
if (mean?.[1]) return cleanQuery(mean[1]);
|
|
16799
|
+
const how = line.match(HOW_CALC_RE);
|
|
16800
|
+
if (how?.[1]) return cleanQuery(how[1]);
|
|
16801
|
+
const what = line.match(WHAT_IS_RE);
|
|
16802
|
+
if (what?.[1]) return cleanQuery(what[1]);
|
|
16803
|
+
return void 0;
|
|
16804
|
+
}
|
|
16805
|
+
function cleanQuery(raw) {
|
|
16806
|
+
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();
|
|
16807
|
+
}
|
|
16808
|
+
function matchDefinitionExplainer(input) {
|
|
16809
|
+
if (!isDefinitionAsk(input)) return void 0;
|
|
16810
|
+
const query = extractDefinitionQuery(input);
|
|
16811
|
+
if (!query) return void 0;
|
|
16812
|
+
const id = resolveMetricId(query);
|
|
16813
|
+
if (!id) {
|
|
16814
|
+
const tokens = query.split(/\s+/);
|
|
16815
|
+
for (let n = tokens.length; n >= 1; n--) {
|
|
16816
|
+
for (let i = 0; i + n <= tokens.length; i++) {
|
|
16817
|
+
const slice = tokens.slice(i, i + n).join(" ");
|
|
16818
|
+
const hit = resolveMetricId(slice);
|
|
16819
|
+
if (hit) return getMetricExplainer(hit);
|
|
16820
|
+
}
|
|
16821
|
+
}
|
|
16822
|
+
return void 0;
|
|
16823
|
+
}
|
|
16824
|
+
return getMetricExplainer(id);
|
|
16825
|
+
}
|
|
16826
|
+
function printWrapped2(text, indent = " ") {
|
|
16827
|
+
for (const line of wrapWords(text, 78)) {
|
|
16828
|
+
console.log(indent + line);
|
|
16829
|
+
}
|
|
16830
|
+
}
|
|
16831
|
+
function tryKeylessDefinitionAnswer(ctx, input) {
|
|
16832
|
+
const explainer = matchDefinitionExplainer(input);
|
|
16833
|
+
if (!explainer) return false;
|
|
16834
|
+
const motion = loadProfile()?.sales_motion ?? null;
|
|
16835
|
+
const bench = explainer.benchmarkHint?.(motion);
|
|
16836
|
+
console.log();
|
|
16837
|
+
console.log(
|
|
16838
|
+
" " + sectionHeading(explainer.label) + chalk16.dim(` \xB7 ${explainer.kind === "vital" ? "vital sign" : "SaaS metric"}`)
|
|
16839
|
+
);
|
|
16840
|
+
console.log(" " + chalk16.dim(explainer.tagline));
|
|
16841
|
+
console.log();
|
|
16842
|
+
console.log(" " + bold("What it means"));
|
|
16843
|
+
printWrapped2(explainer.meaning, " ");
|
|
16844
|
+
console.log();
|
|
16845
|
+
console.log(" " + bold("How NTRP calculates it"));
|
|
16846
|
+
printWrapped2(explainer.how_computed, " ");
|
|
16847
|
+
for (const f of explainer.formula_lines) {
|
|
16848
|
+
console.log(" " + paint("accent", f));
|
|
16849
|
+
}
|
|
16850
|
+
if (bench) {
|
|
16851
|
+
console.log();
|
|
16852
|
+
console.log(" " + chalk16.dim(`Benchmark \xB7 ${bench}`));
|
|
16853
|
+
}
|
|
16854
|
+
if (explainer.dollar_label) {
|
|
16855
|
+
console.log(
|
|
16856
|
+
" " + chalk16.dim(`Dollar translation \xB7 ${explainer.dollar_label}`)
|
|
16857
|
+
);
|
|
16858
|
+
}
|
|
16859
|
+
console.log();
|
|
16860
|
+
console.log(
|
|
16861
|
+
" " + chalk16.dim("More: ") + paint("accent", `/deepdive ${explainer.id}`) + chalk16.dim(" \xB7 full tour: ") + paint("accent", "/deepdive")
|
|
16862
|
+
);
|
|
16863
|
+
console.log();
|
|
16864
|
+
recordMessage(ctx, "user", input);
|
|
16865
|
+
recordMessage(
|
|
16866
|
+
ctx,
|
|
16867
|
+
"agent",
|
|
16868
|
+
`${explainer.label}: ${explainer.tagline} (keyless definition)`
|
|
16869
|
+
);
|
|
16870
|
+
saveSessionState(ctx);
|
|
16871
|
+
return true;
|
|
16872
|
+
}
|
|
16873
|
+
var POSSESSIVE_RE, DEFINITION_RE, MEAN_RE, HOW_CALC_RE, WHAT_IS_RE;
|
|
16874
|
+
var init_keyless_definitions = __esm({
|
|
16875
|
+
"src/conversation/keyless-definitions.ts"() {
|
|
16876
|
+
"use strict";
|
|
16877
|
+
init_context2();
|
|
16878
|
+
init_profile();
|
|
16879
|
+
init_metric_definitions();
|
|
16880
|
+
init_theme();
|
|
16881
|
+
init_layout();
|
|
16882
|
+
POSSESSIVE_RE = /\b(our|my|we|us|the company'?s|this (company|business|org|pipeline)|current|actual|latest)\b/i;
|
|
16883
|
+
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;
|
|
16884
|
+
MEAN_RE = /\bwhat does\b(.+?)\bmean\b/i;
|
|
16885
|
+
HOW_CALC_RE = /\bhow (?:is|are|do(?:es)?)\b(.+?)\b(?:calculated|computed|measured|defined|work)\b/i;
|
|
16886
|
+
WHAT_IS_RE = /\b(?:what(?:'s|s)?|define|explain|describe|tell me about|meaning of)\s+(.+?)(?:\?|$)/i;
|
|
16887
|
+
}
|
|
16888
|
+
});
|
|
16889
|
+
|
|
15616
16890
|
// src/conversation/orchestrator.ts
|
|
15617
|
-
import
|
|
15618
|
-
import { writeFileSync as
|
|
15619
|
-
import { join as join19 } from "path";
|
|
16891
|
+
import chalk17 from "chalk";
|
|
16892
|
+
import { writeFileSync as writeFileSync14 } from "fs";
|
|
15620
16893
|
async function handleExploreWithoutKey(ctx, input) {
|
|
16894
|
+
if (isDefinitionAsk(input) && tryKeylessDefinitionAnswer(ctx, input)) {
|
|
16895
|
+
return;
|
|
16896
|
+
}
|
|
15621
16897
|
if (isKeylessVitalsAsk(input)) {
|
|
15622
16898
|
queuePendingAsk(ctx, input, "explore");
|
|
15623
16899
|
const answered = await tryKeylessAskAnswer(ctx, input);
|
|
@@ -15638,14 +16914,14 @@ async function handleExploreWithoutKey(ctx, input) {
|
|
|
15638
16914
|
}
|
|
15639
16915
|
if (hits === 1) {
|
|
15640
16916
|
console.log();
|
|
15641
|
-
console.log(" " +
|
|
16917
|
+
console.log(" " + chalk17.red("AI interpretation needs an LLM API key saved in config."));
|
|
15642
16918
|
console.log(
|
|
15643
|
-
" " +
|
|
16919
|
+
" " + chalk17.dim("Run ") + paint("accent", "/connect") + chalk17.dim(" and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).")
|
|
15644
16920
|
);
|
|
15645
|
-
console.log(" " +
|
|
16921
|
+
console.log(" " + chalk17.dim("Number crunching works without a key \u2014 only Q&A in the REPL needs one."));
|
|
15646
16922
|
if (ctx.pendingAsk) {
|
|
15647
16923
|
console.log(
|
|
15648
|
-
" " +
|
|
16924
|
+
" " + chalk17.dim("Your question is queued \u2014 I'll answer it right after ") + paint("accent", "/connect") + chalk17.dim(".")
|
|
15649
16925
|
);
|
|
15650
16926
|
}
|
|
15651
16927
|
if (ctx.gapAudit) {
|
|
@@ -15660,16 +16936,17 @@ async function handleExploreWithoutKey(ctx, input) {
|
|
|
15660
16936
|
return;
|
|
15661
16937
|
}
|
|
15662
16938
|
console.log();
|
|
15663
|
-
console.log(" " +
|
|
15664
|
-
console.log(" " +
|
|
15665
|
-
console.log(" " + paint("accent", "/
|
|
15666
|
-
console.log(" " +
|
|
15667
|
-
console.log(" " +
|
|
16939
|
+
console.log(" " + chalk17.yellow("Still no engine connected \u2014 Q&A stays offline until you run ") + paint("accent", "/connect") + chalk17.yellow("."));
|
|
16940
|
+
console.log(" " + chalk17.dim("These work without one:"));
|
|
16941
|
+
console.log(" " + paint("accent", "/deepdive") + chalk17.dim(" metric slides \u2014 what each number means"));
|
|
16942
|
+
console.log(" " + paint("accent", "/playbook") + chalk17.dim(" recommended plays from your computed vitals"));
|
|
16943
|
+
console.log(" " + chalk17.cyan('"how should we fix this?"') + chalk17.dim(" deterministic skeleton plan"));
|
|
16944
|
+
console.log(" " + paint("accent", "/handoff") + chalk17.dim(" export this analysis for another tool"));
|
|
15668
16945
|
console.log();
|
|
15669
16946
|
recordMessage(
|
|
15670
16947
|
ctx,
|
|
15671
16948
|
"agent",
|
|
15672
|
-
"No LLM engine connected \u2014 offered keyless paths (/playbook, skeleton plan, /handoff)."
|
|
16949
|
+
"No LLM engine connected \u2014 offered keyless paths (/deepdive, /playbook, skeleton plan, /handoff)."
|
|
15673
16950
|
);
|
|
15674
16951
|
}
|
|
15675
16952
|
var NO_KEY_NUDGES;
|
|
@@ -15677,7 +16954,7 @@ var init_orchestrator = __esm({
|
|
|
15677
16954
|
"src/conversation/orchestrator.ts"() {
|
|
15678
16955
|
"use strict";
|
|
15679
16956
|
init_context2();
|
|
15680
|
-
|
|
16957
|
+
init_exports_registry();
|
|
15681
16958
|
init_theme();
|
|
15682
16959
|
init_phase();
|
|
15683
16960
|
init_scope();
|
|
@@ -15689,6 +16966,7 @@ var init_orchestrator = __esm({
|
|
|
15689
16966
|
init_time_bank();
|
|
15690
16967
|
init_pending_ask();
|
|
15691
16968
|
init_keyless_ask();
|
|
16969
|
+
init_keyless_definitions();
|
|
15692
16970
|
NO_KEY_NUDGES = /* @__PURE__ */ Symbol.for("ntrp.noKeyNudges");
|
|
15693
16971
|
}
|
|
15694
16972
|
});
|
|
@@ -15854,8 +17132,8 @@ var init_bundle = __esm({
|
|
|
15854
17132
|
});
|
|
15855
17133
|
|
|
15856
17134
|
// src/repositories/markdown.ts
|
|
15857
|
-
import { mkdirSync as
|
|
15858
|
-
import { basename as
|
|
17135
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
|
|
17136
|
+
import { basename as basename4, dirname as dirname3, join as join20, resolve as resolve8 } from "path";
|
|
15859
17137
|
import { stringify as stringifyYaml2 } from "yaml";
|
|
15860
17138
|
function renderMarkdownFiles(pkg) {
|
|
15861
17139
|
const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
|
|
@@ -16042,10 +17320,10 @@ function renderStrategy(entry) {
|
|
|
16042
17320
|
].join("\n");
|
|
16043
17321
|
}
|
|
16044
17322
|
function getRootPath(target) {
|
|
16045
|
-
return
|
|
17323
|
+
return resolve8(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
16046
17324
|
}
|
|
16047
17325
|
function safeFilename(value) {
|
|
16048
|
-
return (
|
|
17326
|
+
return (basename4(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
|
|
16049
17327
|
}
|
|
16050
17328
|
function escapeSummary(value) {
|
|
16051
17329
|
return value.replace(/[<>]/g, "");
|
|
@@ -16058,7 +17336,7 @@ var init_markdown2 = __esm({
|
|
|
16058
17336
|
markdownRepositoryAdapter = {
|
|
16059
17337
|
kind: "markdown",
|
|
16060
17338
|
describeTarget(target) {
|
|
16061
|
-
return target.directory ? `local markdown folder ${
|
|
17339
|
+
return target.directory ? `local markdown folder ${resolve8(target.directory)}` : "local markdown folder";
|
|
16062
17340
|
},
|
|
16063
17341
|
planWrite(pkg) {
|
|
16064
17342
|
const files = renderMarkdownFiles(pkg);
|
|
@@ -16075,12 +17353,12 @@ var init_markdown2 = __esm({
|
|
|
16075
17353
|
write(pkg) {
|
|
16076
17354
|
const root = getRootPath(pkg.target);
|
|
16077
17355
|
const files = renderMarkdownFiles(pkg);
|
|
16078
|
-
|
|
17356
|
+
mkdirSync9(root, { recursive: true });
|
|
16079
17357
|
const written = [];
|
|
16080
17358
|
for (const file of files) {
|
|
16081
17359
|
const absolutePath = join20(root, file.relativePath);
|
|
16082
|
-
|
|
16083
|
-
|
|
17360
|
+
mkdirSync9(dirname3(absolutePath), { recursive: true });
|
|
17361
|
+
writeFileSync15(absolutePath, file.contents, "utf-8");
|
|
16084
17362
|
written.push(absolutePath);
|
|
16085
17363
|
}
|
|
16086
17364
|
return {
|
|
@@ -16316,7 +17594,7 @@ var nl_exports = {};
|
|
|
16316
17594
|
__export(nl_exports, {
|
|
16317
17595
|
runNaturalLanguage: () => runNaturalLanguage
|
|
16318
17596
|
});
|
|
16319
|
-
import
|
|
17597
|
+
import chalk18 from "chalk";
|
|
16320
17598
|
async function runNaturalLanguage(input, ctx) {
|
|
16321
17599
|
if (isSmokeProtocolTrigger(input)) {
|
|
16322
17600
|
recordMessage(ctx, "user", input);
|
|
@@ -16331,7 +17609,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16331
17609
|
return extractSummary(result.answer);
|
|
16332
17610
|
} catch (err) {
|
|
16333
17611
|
spinner2.fail("Smoke protocol failed");
|
|
16334
|
-
console.error(" " +
|
|
17612
|
+
console.error(" " + chalk18.red(String(err.message ?? err)));
|
|
16335
17613
|
console.log();
|
|
16336
17614
|
return;
|
|
16337
17615
|
}
|
|
@@ -16361,8 +17639,8 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16361
17639
|
spinner2.succeed(metricsFirst ? "Session context ready" : "Health snapshot ready");
|
|
16362
17640
|
} catch (err) {
|
|
16363
17641
|
spinner2.fail("Could not compute health snapshot");
|
|
16364
|
-
console.error(" " +
|
|
16365
|
-
console.log(" " +
|
|
17642
|
+
console.error(" " + chalk18.red(String(err.message ?? err)));
|
|
17643
|
+
console.log(" " + chalk18.dim("Run ") + paint("accent", "/new") + chalk18.dim(" \u2192 pick Demo to load sample data."));
|
|
16366
17644
|
console.log();
|
|
16367
17645
|
return;
|
|
16368
17646
|
}
|
|
@@ -16400,7 +17678,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16400
17678
|
break;
|
|
16401
17679
|
case "thinking":
|
|
16402
17680
|
spinner.stop();
|
|
16403
|
-
console.log(" " +
|
|
17681
|
+
console.log(" " + chalk18.dim.italic(event.text));
|
|
16404
17682
|
spinner.start("Thinking\u2026");
|
|
16405
17683
|
break;
|
|
16406
17684
|
case "answer":
|
|
@@ -16420,7 +17698,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16420
17698
|
}
|
|
16421
17699
|
} catch (err) {
|
|
16422
17700
|
spinner.fail("Error while investigating");
|
|
16423
|
-
console.error(" " +
|
|
17701
|
+
console.error(" " + chalk18.red(String(err.message ?? err)));
|
|
16424
17702
|
console.log();
|
|
16425
17703
|
return;
|
|
16426
17704
|
} finally {
|
|
@@ -16430,7 +17708,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16430
17708
|
ctx.conversation = distillThread(rawHistory);
|
|
16431
17709
|
}
|
|
16432
17710
|
if (!lastAnswer) {
|
|
16433
|
-
console.log(" " +
|
|
17711
|
+
console.log(" " + chalk18.dim("(no answer returned)"));
|
|
16434
17712
|
} else {
|
|
16435
17713
|
recordMessage(ctx, "agent", lastAnswer);
|
|
16436
17714
|
if (ctx.pendingAsk) {
|
|
@@ -16462,10 +17740,15 @@ function extractSummary(text) {
|
|
|
16462
17740
|
function printFindingInline(finding) {
|
|
16463
17741
|
const sev = finding.severity;
|
|
16464
17742
|
console.log();
|
|
16465
|
-
console.log(" " + severityPaint(sev)(`[${sev}]`) + " " +
|
|
17743
|
+
console.log(" " + severityPaint(sev)(`[${sev}]`) + " " + chalk18.bold(finding.segment));
|
|
16466
17744
|
printMarkdown(finding.finding, { indent: 2 });
|
|
16467
17745
|
const play = finding.recommended_plays?.[0];
|
|
16468
|
-
if (play) console.log(" " +
|
|
17746
|
+
if (play) console.log(" " + chalk18.dim("\u2192 " + play.play_name + " \u2014 " + play.rationale));
|
|
17747
|
+
if (finding.recommended_focus) {
|
|
17748
|
+
console.log(
|
|
17749
|
+
" " + chalk18.dim("How this works: ") + paint("accent", `/deepdive ${finding.recommended_focus}`)
|
|
17750
|
+
);
|
|
17751
|
+
}
|
|
16469
17752
|
}
|
|
16470
17753
|
var init_nl = __esm({
|
|
16471
17754
|
"src/cli/nl.ts"() {
|
|
@@ -16499,12 +17782,12 @@ __export(demo_exports, {
|
|
|
16499
17782
|
printDemoDisabled: () => printDemoDisabled,
|
|
16500
17783
|
setDemoEnabled: () => setDemoEnabled
|
|
16501
17784
|
});
|
|
16502
|
-
import
|
|
17785
|
+
import chalk19 from "chalk";
|
|
16503
17786
|
function printDemoDisabled() {
|
|
16504
17787
|
console.log();
|
|
16505
|
-
console.log(" " +
|
|
17788
|
+
console.log(" " + chalk19.red(DEMO_DISABLED_MESSAGE));
|
|
16506
17789
|
console.log(
|
|
16507
|
-
" " +
|
|
17790
|
+
" " + chalk19.dim("Re-enable with ") + paint("accent", "/config set demo-enabled true") + chalk19.dim(".")
|
|
16508
17791
|
);
|
|
16509
17792
|
console.log();
|
|
16510
17793
|
}
|
|
@@ -16546,7 +17829,7 @@ __export(pending_ask_exports, {
|
|
|
16546
17829
|
queuePendingAsk: () => queuePendingAsk,
|
|
16547
17830
|
resumePendingAsk: () => resumePendingAsk
|
|
16548
17831
|
});
|
|
16549
|
-
import
|
|
17832
|
+
import chalk20 from "chalk";
|
|
16550
17833
|
function looksLikeQuestion(input) {
|
|
16551
17834
|
const text = input.trim();
|
|
16552
17835
|
if (!text) return false;
|
|
@@ -16582,7 +17865,7 @@ function printFocusChip(ctx) {
|
|
|
16582
17865
|
const period = ctx.scope.time_horizon ? ` \xB7 ${ctx.scope.time_horizon}` : "";
|
|
16583
17866
|
console.log();
|
|
16584
17867
|
console.log(
|
|
16585
|
-
" " +
|
|
17868
|
+
" " + chalk20.dim("Focus: ") + paint("accent", lens) + chalk20.dim(period) + chalk20.dim(" \u2014 type ") + chalk20.cyan("adjust") + chalk20.dim(" to change")
|
|
16586
17869
|
);
|
|
16587
17870
|
console.log();
|
|
16588
17871
|
}
|
|
@@ -16593,7 +17876,7 @@ async function resumePendingAsk(ctx) {
|
|
|
16593
17876
|
if (canUseReplAi(ctx)) {
|
|
16594
17877
|
console.log();
|
|
16595
17878
|
console.log(
|
|
16596
|
-
" " +
|
|
17879
|
+
" " + chalk20.dim(
|
|
16597
17880
|
pending.keylessAnswered ? "Picking up your question with the connected engine\u2026" : "Picking up your question\u2026"
|
|
16598
17881
|
)
|
|
16599
17882
|
);
|
|
@@ -16626,7 +17909,7 @@ async function offerDemoToAnswer(ctx) {
|
|
|
16626
17909
|
const go = await prompts.confirm("Use demo data to answer this?", true);
|
|
16627
17910
|
if (!go) {
|
|
16628
17911
|
console.log(
|
|
16629
|
-
" " +
|
|
17912
|
+
" " + chalk20.dim("Paste a CSV path when ready, or say ") + chalk20.cyan("use demo data") + chalk20.dim(".")
|
|
16630
17913
|
);
|
|
16631
17914
|
console.log();
|
|
16632
17915
|
return false;
|
|
@@ -16658,7 +17941,7 @@ __export(compute_exports2, {
|
|
|
16658
17941
|
isComputeIntent: () => isComputeIntent,
|
|
16659
17942
|
runConversationCompute: () => runConversationCompute
|
|
16660
17943
|
});
|
|
16661
|
-
import
|
|
17944
|
+
import chalk21 from "chalk";
|
|
16662
17945
|
async function runConversationCompute(ctx) {
|
|
16663
17946
|
const lens = ctx.scope?.primary_lens ?? ctx.analysis.primary;
|
|
16664
17947
|
ctx.computeInProgress = true;
|
|
@@ -16713,7 +17996,7 @@ async function runConversationCompute(ctx) {
|
|
|
16713
17996
|
creditGapCompute(ctx);
|
|
16714
17997
|
return typeof summary === "string" ? summary : "Health analysis ready";
|
|
16715
17998
|
} catch (err) {
|
|
16716
|
-
console.error(" " +
|
|
17999
|
+
console.error(" " + chalk21.red(String(err.message ?? err)));
|
|
16717
18000
|
return;
|
|
16718
18001
|
} finally {
|
|
16719
18002
|
ctx.computeInProgress = false;
|
|
@@ -19482,18 +20765,18 @@ var init_generator = __esm({
|
|
|
19482
20765
|
});
|
|
19483
20766
|
|
|
19484
20767
|
// src/demo/taxonomy-cache.ts
|
|
19485
|
-
import { readFileSync as
|
|
19486
|
-
import { homedir as
|
|
20768
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync16, existsSync as existsSync19, mkdirSync as mkdirSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
20769
|
+
import { homedir as homedir6 } from "os";
|
|
19487
20770
|
import { join as join22 } from "path";
|
|
19488
20771
|
function ensureDir5() {
|
|
19489
|
-
if (!
|
|
19490
|
-
|
|
20772
|
+
if (!existsSync19(NTRP_DIR4)) {
|
|
20773
|
+
mkdirSync10(NTRP_DIR4, { recursive: true });
|
|
19491
20774
|
}
|
|
19492
20775
|
}
|
|
19493
20776
|
function loadCachedTaxonomy(profile) {
|
|
19494
|
-
if (!
|
|
20777
|
+
if (!existsSync19(TAXONOMY_PATH)) return null;
|
|
19495
20778
|
try {
|
|
19496
|
-
const parsed = JSON.parse(
|
|
20779
|
+
const parsed = JSON.parse(readFileSync18(TAXONOMY_PATH, "utf-8"));
|
|
19497
20780
|
if (!parsed || typeof parsed !== "object") return null;
|
|
19498
20781
|
if (parsed.profile_updated_at !== profile.updated_at) return null;
|
|
19499
20782
|
return parsed;
|
|
@@ -19503,13 +20786,13 @@ function loadCachedTaxonomy(profile) {
|
|
|
19503
20786
|
}
|
|
19504
20787
|
function saveCachedTaxonomy(taxonomy) {
|
|
19505
20788
|
ensureDir5();
|
|
19506
|
-
|
|
20789
|
+
writeFileSync16(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
|
|
19507
20790
|
}
|
|
19508
20791
|
var NTRP_DIR4, TAXONOMY_PATH;
|
|
19509
20792
|
var init_taxonomy_cache = __esm({
|
|
19510
20793
|
"src/demo/taxonomy-cache.ts"() {
|
|
19511
20794
|
"use strict";
|
|
19512
|
-
NTRP_DIR4 = join22(
|
|
20795
|
+
NTRP_DIR4 = join22(homedir6(), ".ntrp");
|
|
19513
20796
|
TAXONOMY_PATH = join22(NTRP_DIR4, "demo-taxonomy.json");
|
|
19514
20797
|
}
|
|
19515
20798
|
});
|
|
@@ -19746,16 +21029,16 @@ var generate_exports = {};
|
|
|
19746
21029
|
__export(generate_exports, {
|
|
19747
21030
|
handler: () => handler2
|
|
19748
21031
|
});
|
|
19749
|
-
import
|
|
21032
|
+
import chalk22 from "chalk";
|
|
19750
21033
|
async function handler2(args, ctx) {
|
|
19751
21034
|
const { flags } = parseArgs(args, ["list-scenarios", "regen-taxonomy", "brief"]);
|
|
19752
21035
|
const quiet = ctx.execution.quiet;
|
|
19753
21036
|
const brief = getBool(flags, "brief");
|
|
19754
21037
|
if (getBool(flags, "list-scenarios")) {
|
|
19755
|
-
console.log(
|
|
21038
|
+
console.log(chalk22.bold("\n Available Scenarios:\n"));
|
|
19756
21039
|
for (const s of SCENARIO_LIST) {
|
|
19757
|
-
console.log(` ${
|
|
19758
|
-
console.log(` ${
|
|
21040
|
+
console.log(` ${chalk22.cyan(s.key.padEnd(20))} ${s.label}`);
|
|
21041
|
+
console.log(` ${chalk22.dim(" ".repeat(20))} ${s.description}
|
|
19759
21042
|
`);
|
|
19760
21043
|
}
|
|
19761
21044
|
return true;
|
|
@@ -19765,9 +21048,9 @@ async function handler2(args, ctx) {
|
|
|
19765
21048
|
const skipProfile = getFalse(flags, "profile");
|
|
19766
21049
|
if (!isProfileConfigured(profile) && !skipProfile) {
|
|
19767
21050
|
console.error();
|
|
19768
|
-
console.error(" " +
|
|
19769
|
-
console.error(" " +
|
|
19770
|
-
console.error(" " +
|
|
21051
|
+
console.error(" " + chalk22.red("No company profile found."));
|
|
21052
|
+
console.error(" " + chalk22.dim("Run ") + paint("accent", "/onboard") + chalk22.dim(" first for a richer demo,"));
|
|
21053
|
+
console.error(" " + chalk22.dim("or pass ") + paint("accent", "--no-profile") + chalk22.dim(" to skip."));
|
|
19771
21054
|
console.error();
|
|
19772
21055
|
markFailure(ctx);
|
|
19773
21056
|
return false;
|
|
@@ -19775,8 +21058,8 @@ async function handler2(args, ctx) {
|
|
|
19775
21058
|
const explicitScenario = getString(flags, "scenario", "s");
|
|
19776
21059
|
const resolvedScenario = resolveScenarioInput(explicitScenario);
|
|
19777
21060
|
if (resolvedScenario === null) {
|
|
19778
|
-
console.error(
|
|
19779
|
-
console.log(
|
|
21061
|
+
console.error(chalk22.red(` Unknown scenario: ${explicitScenario}`));
|
|
21062
|
+
console.log(chalk22.dim(` Valid: ${SCENARIO_LIST.map((s) => s.key).join(", ")}`));
|
|
19780
21063
|
markFailure(ctx);
|
|
19781
21064
|
return false;
|
|
19782
21065
|
}
|
|
@@ -19790,10 +21073,10 @@ async function handler2(args, ctx) {
|
|
|
19790
21073
|
const s = getScenario(scenario);
|
|
19791
21074
|
console.log();
|
|
19792
21075
|
if (brief) {
|
|
19793
|
-
console.log(" " + paint("accent", "\u2713 Demo: ") + bold(s.label) +
|
|
21076
|
+
console.log(" " + paint("accent", "\u2713 Demo: ") + bold(s.label) + chalk22.dim(" \u2014 " + s.hook));
|
|
19794
21077
|
} else {
|
|
19795
21078
|
console.log(" " + paint("accent", "\u2713 Scenario: ") + bold(s.label));
|
|
19796
|
-
console.log(" " +
|
|
21079
|
+
console.log(" " + chalk22.dim(s.story));
|
|
19797
21080
|
console.log();
|
|
19798
21081
|
}
|
|
19799
21082
|
}
|
|
@@ -19823,18 +21106,18 @@ async function handler2(args, ctx) {
|
|
|
19823
21106
|
if (brief) {
|
|
19824
21107
|
spinner.succeed(`Demo loaded \u2014 ${briefCounts(result.counts)}`);
|
|
19825
21108
|
} else {
|
|
19826
|
-
spinner.succeed(`Generated demo data for "${
|
|
21109
|
+
spinner.succeed(`Generated demo data for "${chalk22.cyan(scenario)}" scenario`);
|
|
19827
21110
|
console.log();
|
|
19828
21111
|
printEntityCounts(result.counts);
|
|
19829
21112
|
}
|
|
19830
21113
|
}
|
|
19831
21114
|
if (!quiet && !brief && ctx.analysis.primary !== "revenue_metrics") {
|
|
19832
|
-
console.log(
|
|
21115
|
+
console.log(chalk22.dim("\n Run /diagnose to compute vital signs.\n"));
|
|
19833
21116
|
}
|
|
19834
21117
|
}
|
|
19835
21118
|
} catch (err) {
|
|
19836
21119
|
if (spinner) spinner.fail("Generation failed");
|
|
19837
|
-
console.error(
|
|
21120
|
+
console.error(chalk22.red(String(err)));
|
|
19838
21121
|
markFailure(ctx);
|
|
19839
21122
|
return false;
|
|
19840
21123
|
}
|
|
@@ -19872,7 +21155,7 @@ async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
|
|
|
19872
21155
|
return taxonomy;
|
|
19873
21156
|
} catch (err) {
|
|
19874
21157
|
spinner.fail("Couldn't build market taxonomy \u2014 using generic data pools");
|
|
19875
|
-
console.log(" " +
|
|
21158
|
+
console.log(" " + chalk22.dim(String(err.message ?? err)));
|
|
19876
21159
|
return void 0;
|
|
19877
21160
|
}
|
|
19878
21161
|
}
|
|
@@ -19963,9 +21246,9 @@ var ingest_exports = {};
|
|
|
19963
21246
|
__export(ingest_exports, {
|
|
19964
21247
|
handler: () => handler3
|
|
19965
21248
|
});
|
|
19966
|
-
import
|
|
19967
|
-
import { readFileSync as
|
|
19968
|
-
import { basename as
|
|
21249
|
+
import chalk23 from "chalk";
|
|
21250
|
+
import { readFileSync as readFileSync19, existsSync as existsSync20 } from "fs";
|
|
21251
|
+
import { basename as basename5 } from "path";
|
|
19969
21252
|
async function handler3(args, ctx) {
|
|
19970
21253
|
const { positional, flags } = parseArgs(args, [
|
|
19971
21254
|
"skip-resolve",
|
|
@@ -19984,21 +21267,21 @@ async function handler3(args, ctx) {
|
|
|
19984
21267
|
const source = getString(flags, "source", "s") ?? "salesforce";
|
|
19985
21268
|
const skipResolve = getBool(flags, "skip-resolve");
|
|
19986
21269
|
if (!file) {
|
|
19987
|
-
console.error(
|
|
19988
|
-
console.error(
|
|
21270
|
+
console.error(chalk23.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
|
|
21271
|
+
console.error(chalk23.dim(" /ingest --demo [--scenario <name>]"));
|
|
19989
21272
|
process.exit(1);
|
|
19990
21273
|
}
|
|
19991
|
-
if (!
|
|
19992
|
-
console.error(
|
|
21274
|
+
if (!existsSync20(file)) {
|
|
21275
|
+
console.error(chalk23.red(` File not found: ${file}`));
|
|
19993
21276
|
process.exit(1);
|
|
19994
21277
|
}
|
|
19995
21278
|
const profile = loadProfile();
|
|
19996
21279
|
const skipProfile = getFalse(flags, "profile");
|
|
19997
21280
|
if (!profile && !skipProfile) {
|
|
19998
21281
|
console.error();
|
|
19999
|
-
console.error(" " +
|
|
20000
|
-
console.error(" " +
|
|
20001
|
-
console.error(" " +
|
|
21282
|
+
console.error(" " + chalk23.red("No company profile found."));
|
|
21283
|
+
console.error(" " + chalk23.dim("Run ") + paint("accent", "/onboard") + chalk23.dim(" first for better column mapping,"));
|
|
21284
|
+
console.error(" " + chalk23.dim("or pass ") + paint("accent", "--no-profile") + chalk23.dim(" to skip."));
|
|
20002
21285
|
console.error();
|
|
20003
21286
|
process.exit(1);
|
|
20004
21287
|
}
|
|
@@ -20006,7 +21289,7 @@ async function handler3(args, ctx) {
|
|
|
20006
21289
|
try {
|
|
20007
21290
|
await initSchema();
|
|
20008
21291
|
spinner.text = "Parsing CSV\u2026";
|
|
20009
|
-
const content =
|
|
21292
|
+
const content = readFileSync19(file, "utf-8");
|
|
20010
21293
|
const { rows, headers } = parseCSV(content);
|
|
20011
21294
|
if (rows.length === 0) {
|
|
20012
21295
|
spinner.fail("CSV is empty");
|
|
@@ -20018,7 +21301,7 @@ async function handler3(args, ctx) {
|
|
|
20018
21301
|
const { importRevenueRows: importRevenueRows2 } = await Promise.resolve().then(() => (init_revenue_importer(), revenue_importer_exports));
|
|
20019
21302
|
const uploadId2 = await insertCSVUpload({
|
|
20020
21303
|
source_system: source,
|
|
20021
|
-
original_filename:
|
|
21304
|
+
original_filename: basename5(file),
|
|
20022
21305
|
row_count: rows.length,
|
|
20023
21306
|
column_mappings: { entity_type: "revenue_ledger" },
|
|
20024
21307
|
status: "processing"
|
|
@@ -20030,28 +21313,28 @@ async function handler3(args, ctx) {
|
|
|
20030
21313
|
row_count: result2.imported
|
|
20031
21314
|
});
|
|
20032
21315
|
spinner.succeed(
|
|
20033
|
-
`Imported ${
|
|
21316
|
+
`Imported ${chalk23.bold(result2.imported.toString())} revenue events from ${chalk23.dim(basename5(file))}`
|
|
20034
21317
|
);
|
|
20035
21318
|
if (result2.errors.length > 0) {
|
|
20036
|
-
console.log(
|
|
21319
|
+
console.log(chalk23.yellow(` ${result2.errors.length} rows skipped`));
|
|
20037
21320
|
}
|
|
20038
21321
|
if (ctx.analysis) {
|
|
20039
21322
|
ctx.analysis.data_source_type = "revenue_ledger";
|
|
20040
21323
|
}
|
|
20041
|
-
console.log(
|
|
20042
|
-
return `${result2.imported} revenue events from ${
|
|
21324
|
+
console.log(chalk23.dim(" Run ") + chalk23.cyan("/metrics") + chalk23.dim(" for SaaS metrics with ledger-backed retention."));
|
|
21325
|
+
return `${result2.imported} revenue events from ${basename5(file)}`;
|
|
20043
21326
|
}
|
|
20044
21327
|
spinner.text = "Detecting entity type\u2026";
|
|
20045
21328
|
const detection = detectEntityType(headers, source);
|
|
20046
21329
|
if (!detection) {
|
|
20047
21330
|
spinner.fail(`Could not auto-detect entity type for source: ${source}`);
|
|
20048
|
-
console.log(
|
|
21331
|
+
console.log(chalk23.dim(" Headers found: " + headers.join(", ")));
|
|
20049
21332
|
process.exit(1);
|
|
20050
21333
|
}
|
|
20051
21334
|
spinner.text = `Importing ${rows.length} ${detection.entityType} rows...`;
|
|
20052
21335
|
const uploadId = await insertCSVUpload({
|
|
20053
21336
|
source_system: source,
|
|
20054
|
-
original_filename:
|
|
21337
|
+
original_filename: basename5(file),
|
|
20055
21338
|
row_count: rows.length,
|
|
20056
21339
|
column_mappings: detection.mappings,
|
|
20057
21340
|
status: "processing"
|
|
@@ -20068,15 +21351,15 @@ async function handler3(args, ctx) {
|
|
|
20068
21351
|
row_count: result.imported
|
|
20069
21352
|
});
|
|
20070
21353
|
spinner.succeed(
|
|
20071
|
-
`Imported ${
|
|
21354
|
+
`Imported ${chalk23.bold(result.imported.toString())} ${detection.entityType} from ${chalk23.dim(basename5(file))} (${source})`
|
|
20072
21355
|
);
|
|
20073
21356
|
if (result.errors.length > 0) {
|
|
20074
|
-
console.log(
|
|
21357
|
+
console.log(chalk23.yellow(` ${result.errors.length} rows skipped`));
|
|
20075
21358
|
for (const err of result.errors.slice(0, 3)) {
|
|
20076
|
-
console.log(
|
|
21359
|
+
console.log(chalk23.dim(` - ${err}`));
|
|
20077
21360
|
}
|
|
20078
21361
|
if (result.errors.length > 3) {
|
|
20079
|
-
console.log(
|
|
21362
|
+
console.log(chalk23.dim(` ... and ${result.errors.length - 3} more`));
|
|
20080
21363
|
}
|
|
20081
21364
|
}
|
|
20082
21365
|
if (!skipResolve) {
|
|
@@ -20090,10 +21373,10 @@ async function handler3(args, ctx) {
|
|
|
20090
21373
|
resolveSpinner.succeed("No duplicates found");
|
|
20091
21374
|
}
|
|
20092
21375
|
}
|
|
20093
|
-
return `${result.imported} ${detection.entityType} from ${
|
|
21376
|
+
return `${result.imported} ${detection.entityType} from ${basename5(file)}`;
|
|
20094
21377
|
} catch (err) {
|
|
20095
21378
|
spinner.fail("Import failed");
|
|
20096
|
-
console.error(
|
|
21379
|
+
console.error(chalk23.red(String(err)));
|
|
20097
21380
|
process.exit(1);
|
|
20098
21381
|
}
|
|
20099
21382
|
}
|
|
@@ -20123,10 +21406,10 @@ __export(ingest_chat_exports, {
|
|
|
20123
21406
|
loadDemoFromChat: () => loadDemoFromChat,
|
|
20124
21407
|
looksLikeFilePath: () => looksLikeFilePath
|
|
20125
21408
|
});
|
|
20126
|
-
import { existsSync as
|
|
20127
|
-
import { basename as
|
|
20128
|
-
import { homedir as
|
|
20129
|
-
import
|
|
21409
|
+
import { existsSync as existsSync21 } from "fs";
|
|
21410
|
+
import { basename as basename6, resolve as resolve9 } from "path";
|
|
21411
|
+
import { homedir as homedir7 } from "os";
|
|
21412
|
+
import chalk24 from "chalk";
|
|
20130
21413
|
function extractFilePath(input) {
|
|
20131
21414
|
const trimmed = input.trim();
|
|
20132
21415
|
const patterns = [
|
|
@@ -20143,33 +21426,33 @@ function extractFilePath(input) {
|
|
|
20143
21426
|
const m = trimmed.match(re);
|
|
20144
21427
|
if (m?.[1]) {
|
|
20145
21428
|
const p = expandPath(m[1]);
|
|
20146
|
-
if (
|
|
21429
|
+
if (existsSync21(p)) return p;
|
|
20147
21430
|
}
|
|
20148
21431
|
if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
|
|
20149
21432
|
const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
|
|
20150
|
-
if (
|
|
21433
|
+
if (existsSync21(p)) return p;
|
|
20151
21434
|
}
|
|
20152
21435
|
}
|
|
20153
21436
|
return null;
|
|
20154
21437
|
}
|
|
20155
21438
|
function expandPath(p) {
|
|
20156
|
-
if (p.startsWith("~/")) return
|
|
20157
|
-
return
|
|
21439
|
+
if (p.startsWith("~/")) return resolve9(homedir7(), p.slice(2));
|
|
21440
|
+
return resolve9(p);
|
|
20158
21441
|
}
|
|
20159
21442
|
function looksLikeFilePath(input) {
|
|
20160
21443
|
return extractFilePath(input) !== null;
|
|
20161
21444
|
}
|
|
20162
21445
|
async function ingestFromChat(ctx, filePath) {
|
|
20163
21446
|
if (!ctx.rl) {
|
|
20164
|
-
console.log(" " +
|
|
21447
|
+
console.log(" " + chalk24.red("Ingest confirm requires interactive mode."));
|
|
20165
21448
|
return false;
|
|
20166
21449
|
}
|
|
20167
|
-
const name =
|
|
21450
|
+
const name = basename6(filePath);
|
|
20168
21451
|
const prompts = createPromptSession(ctx.rl, ctx);
|
|
20169
21452
|
try {
|
|
20170
21453
|
const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
|
|
20171
21454
|
if (!ok) {
|
|
20172
|
-
console.log(" " +
|
|
21455
|
+
console.log(" " + chalk24.dim("Ingest cancelled."));
|
|
20173
21456
|
return false;
|
|
20174
21457
|
}
|
|
20175
21458
|
} finally {
|
|
@@ -20177,12 +21460,12 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20177
21460
|
}
|
|
20178
21461
|
const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
|
|
20179
21462
|
const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
|
|
20180
|
-
const { readFileSync:
|
|
21463
|
+
const { readFileSync: readFileSync21 } = await import("fs");
|
|
20181
21464
|
const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
|
|
20182
21465
|
const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
|
|
20183
21466
|
let headerCheckFailed = false;
|
|
20184
21467
|
try {
|
|
20185
|
-
const raw =
|
|
21468
|
+
const raw = readFileSync21(filePath, "utf-8");
|
|
20186
21469
|
const { headers } = parseCSV2(raw);
|
|
20187
21470
|
const detected = detectEntityType2(headers, "unknown");
|
|
20188
21471
|
if (!detected) headerCheckFailed = true;
|
|
@@ -20197,7 +21480,7 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20197
21480
|
false
|
|
20198
21481
|
);
|
|
20199
21482
|
if (useAi) {
|
|
20200
|
-
console.log(" " +
|
|
21483
|
+
console.log(" " + chalk24.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
|
|
20201
21484
|
}
|
|
20202
21485
|
} finally {
|
|
20203
21486
|
prompts2.close();
|
|
@@ -20224,7 +21507,7 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20224
21507
|
invalidateGapAudit(ctx);
|
|
20225
21508
|
saveSessionState(ctx);
|
|
20226
21509
|
console.log();
|
|
20227
|
-
console.log(" " + paint("accent", "\u2713 Data loaded") +
|
|
21510
|
+
console.log(" " + paint("accent", "\u2713 Data loaded") + chalk24.dim(` \u2014 ${name}`));
|
|
20228
21511
|
recordMessage(ctx, "user", `[ingested ${name}]`);
|
|
20229
21512
|
recordMessage(ctx, "agent", `Loaded ${name}. Checking what we can analyze\u2026`);
|
|
20230
21513
|
const audit = await refreshGapAudit(ctx);
|
|
@@ -20232,7 +21515,7 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20232
21515
|
if (audit.can_compute && ctx.scope?.confirmed_at) {
|
|
20233
21516
|
if (ctx.pendingAsk) {
|
|
20234
21517
|
console.log();
|
|
20235
|
-
console.log(" " +
|
|
21518
|
+
console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
|
|
20236
21519
|
await runConversationCompute(ctx);
|
|
20237
21520
|
return true;
|
|
20238
21521
|
}
|
|
@@ -20294,7 +21577,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
|
|
|
20294
21577
|
const shouldAuto = opts.autoCompute || Boolean(ctx.pendingAsk && audit.can_compute && ctx.scope?.confirmed_at);
|
|
20295
21578
|
if (shouldAuto && audit.can_compute) {
|
|
20296
21579
|
console.log();
|
|
20297
|
-
console.log(" " +
|
|
21580
|
+
console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
|
|
20298
21581
|
await runConversationCompute(ctx);
|
|
20299
21582
|
return true;
|
|
20300
21583
|
}
|
|
@@ -20783,9 +22066,9 @@ async function handleGetSessionBrief(input) {
|
|
|
20783
22066
|
if (!target) {
|
|
20784
22067
|
return { error: `No session matching "${raw}".` };
|
|
20785
22068
|
}
|
|
20786
|
-
const { existsSync:
|
|
22069
|
+
const { existsSync: existsSync23, readFileSync: readFileSync21 } = await import("fs");
|
|
20787
22070
|
const briefPath = contextDocPathForSession2(target.id);
|
|
20788
|
-
if (!
|
|
22071
|
+
if (!existsSync23(briefPath)) {
|
|
20789
22072
|
return {
|
|
20790
22073
|
session_id: target.id,
|
|
20791
22074
|
error: "No context brief on disk for this session (created before brief storage existed).",
|
|
@@ -20793,7 +22076,7 @@ async function handleGetSessionBrief(input) {
|
|
|
20793
22076
|
stage: target.stage ?? null
|
|
20794
22077
|
};
|
|
20795
22078
|
}
|
|
20796
|
-
return { session_id: target.id, brief:
|
|
22079
|
+
return { session_id: target.id, brief: readFileSync21(briefPath, "utf-8") };
|
|
20797
22080
|
}
|
|
20798
22081
|
function auditDenied(name, input, resultJson, start) {
|
|
20799
22082
|
logToolCall({
|
|
@@ -21331,15 +22614,15 @@ init_phase();
|
|
|
21331
22614
|
init_store2();
|
|
21332
22615
|
init_session_analysis();
|
|
21333
22616
|
init_strategist();
|
|
21334
|
-
import { mkdirSync as
|
|
21335
|
-
import { dirname as
|
|
22617
|
+
import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync17, readFileSync as readFileSync20, existsSync as existsSync22 } from "fs";
|
|
22618
|
+
import { dirname as dirname4, join as join23, resolve as resolve10 } from "path";
|
|
21336
22619
|
import { fileURLToPath } from "url";
|
|
21337
|
-
var HERE =
|
|
21338
|
-
var REPO_ROOT =
|
|
22620
|
+
var HERE = dirname4(fileURLToPath(import.meta.url));
|
|
22621
|
+
var REPO_ROOT = resolve10(HERE, "../..");
|
|
21339
22622
|
var EVAL_DIR = join23(REPO_ROOT, "eval");
|
|
21340
22623
|
var INPUTS_PATH = join23(EVAL_DIR, "inputs.json");
|
|
21341
22624
|
function loadEvalInputs(path = INPUTS_PATH) {
|
|
21342
|
-
return JSON.parse(
|
|
22625
|
+
return JSON.parse(readFileSync20(path, "utf-8"));
|
|
21343
22626
|
}
|
|
21344
22627
|
async function ensureDemoAndDiagnosis(ctx, inputs) {
|
|
21345
22628
|
await initSchema();
|
|
@@ -21615,7 +22898,7 @@ async function runQualityEval(options = {}) {
|
|
|
21615
22898
|
if (options.quick) {
|
|
21616
22899
|
fixtures = fixtures.filter((f) => f.surface === "ask" && f.response_mode === "brief");
|
|
21617
22900
|
}
|
|
21618
|
-
|
|
22901
|
+
mkdirSync11(outDir, { recursive: true });
|
|
21619
22902
|
const ctx = initContext(true, { mode: "investigation", output: "json" });
|
|
21620
22903
|
await ensureDemoAndDiagnosis(ctx, inputs);
|
|
21621
22904
|
if (!canUseReplAi(ctx)) {
|
|
@@ -21658,8 +22941,8 @@ async function runQualityEval(options = {}) {
|
|
|
21658
22941
|
duration_ms: Date.now() - started
|
|
21659
22942
|
};
|
|
21660
22943
|
results.push(result);
|
|
21661
|
-
|
|
21662
|
-
|
|
22944
|
+
writeFileSync17(join23(outDir, `${fixture.id}.json`), JSON.stringify(result, null, 2) + "\n");
|
|
22945
|
+
writeFileSync17(join23(outDir, `${fixture.id}.md`), `# ${fixture.id} (${fixture.surface})
|
|
21663
22946
|
|
|
21664
22947
|
Input: ${fixture.input ?? "(none)"}
|
|
21665
22948
|
|
|
@@ -21677,7 +22960,7 @@ ${result.output_text}
|
|
|
21677
22960
|
duration_ms: Date.now() - started
|
|
21678
22961
|
};
|
|
21679
22962
|
results.push(result);
|
|
21680
|
-
|
|
22963
|
+
writeFileSync17(join23(outDir, `${fixture.id}.json`), JSON.stringify(result, null, 2) + "\n");
|
|
21681
22964
|
console.error(`[eval] ${fixture.id} FAILED: ${err.message}`);
|
|
21682
22965
|
}
|
|
21683
22966
|
}
|
|
@@ -21688,7 +22971,7 @@ ${result.output_text}
|
|
|
21688
22971
|
errors: results.filter((r) => r.error).map((r) => r.id),
|
|
21689
22972
|
models: [...new Set(results.map((r) => r.model_used).filter(Boolean))]
|
|
21690
22973
|
};
|
|
21691
|
-
|
|
22974
|
+
writeFileSync17(join23(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
21692
22975
|
return {
|
|
21693
22976
|
iteration,
|
|
21694
22977
|
out_dir: outDir,
|